Source: core/sceneObjs/other/Detector.js

/*
 * 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 BaseSceneObj from '../BaseSceneObj.js';
import LineObjMixin from '../LineObjMixin.js';
import i18next from 'i18next';
import geometry from '../../geometry.js';

/**
 * The detector tool
 * 
 * Tools -> Other -> Detector
 * @class
 * @extends BaseSceneObj
 * @memberof sceneObjs
 * @property {Point} p1 - The first endpoint of the line segment.
 * @property {Point} p2 - The second endpoint of the line segment.
 * @property {boolean} irradMap - Whether to display the irradiance map.
 * @property {number} binSize - The size of the bin for the irradiance map.
 * @property {number} power - The measured power through the detector.
 * @property {number} normal - The measured normal force through the detector.
 * @property {number} shear - The measured shear force through the detector.
 * @property {Array<number>} binData - The measured data for the irradiance map.
 */
class Detector extends LineObjMixin(BaseSceneObj) {
  static type = 'Detector';
  static isOptical = true;
  static serializableDefaults = {
    p1: null,
    p2: null,
    irradMap: false,
    binSize: 1
  };

  constructor(scene, properties) {
    super(scene, properties);

    // Initialize the quantities to be measured
    this.power = 0;
    this.normal = 0;
    this.shear = 0;
    this.binData = null;
  }

  populateObjBar(objBar) {
    if (this.scene.colorMode !== 'default') {
      var sInfo = i18next.t('simulator:sceneObjs.Detector.info.sNewColorModes');
    } else {
      var sInfo = i18next.t('simulator:sceneObjs.Detector.info.s');
    }
    objBar.setTitle(i18next.t('main:tools.Detector.title'));
    objBar.createInfoBox('<ul><li>' + i18next.t('simulator:sceneObjs.Detector.info.P') + '</li><li>' + i18next.t('simulator:sceneObjs.Detector.info.Fperp') + '</li><li>' + i18next.t('simulator:sceneObjs.Detector.info.Fpar') + '</li><li>' + i18next.t('simulator:sceneObjs.Detector.info.irradiance') + '</li><li>' + i18next.t('simulator:sceneObjs.Detector.info.length') + '</li><li>' + i18next.t('simulator:sceneObjs.Detector.info.B') + '</li><li>' + sInfo + '</li><li>' + i18next.t('simulator:sceneObjs.Detector.info.truncation') + '</li></ul>');

    objBar.createBoolean(i18next.t('simulator:sceneObjs.Detector.irradMap'), this.irradMap, function (obj, value) {
      obj.irradMap = value;
    }, null, true);

    if (this.irradMap) {
      objBar.createNumber(i18next.t('simulator:sceneObjs.Detector.binSize'), 0.01 * this.scene.lengthScale, 10 * this.scene.lengthScale, 0.01 * this.scene.lengthScale, this.binSize, function (obj, value) {
        obj.binSize = value;
      }, i18next.t('simulator:sceneObjs.common.lengthUnitInfo'));

      const self = this;

      objBar.createButton(i18next.t('simulator:sceneObjs.Detector.exportData'), function (obj) {
        // Export the irradiance map to a CSV file
        var binSize = obj.binSize;
        var binNum = Math.ceil(Math.sqrt((obj.p2.x - obj.p1.x) * (obj.p2.x - obj.p1.x) + (obj.p2.y - obj.p1.y) * (obj.p2.y - obj.p1.y)) / binSize);
        var binData = obj.binData;
        var csv = "data:text/csv;charset=utf-8,";

        // Write the header
        csv += "Position,Irradiance\n";

        // Write the data
        for (var i = 0; i < binNum; i++) {
          csv += i * binSize + "," + (binData[i] / binSize) + "\n";
        }
        var encodedUri = encodeURI(csv);

        // Download the file
        var link = document.createElement("a");
        link.setAttribute("href", encodedUri);
        link.setAttribute("download", (self.scene.name || "irradiance_map") + ".csv");
        document.body.appendChild(link);
        link.click();
      });
    }
  }

  draw(canvasRenderer, isAboveLight, isHovered) {
    const ctx = canvasRenderer.ctx;
    const ls = canvasRenderer.lengthScale;

    if (!isAboveLight) {
      ctx.globalCompositeOperation = 'lighter';

      ctx.strokeStyle = isHovered ? 'cyan' : ('rgb(192,192,192)');
      ctx.lineWidth = 1 * ls;
      ctx.setLineDash([5 * ls, 5 * ls]);
      ctx.beginPath();
      ctx.moveTo(this.p1.x, this.p1.y);
      ctx.lineTo(this.p2.x, this.p2.y);
      ctx.stroke();
      ctx.setLineDash([]);

      ctx.globalCompositeOperation = 'source-over';
    } else {
      ctx.globalCompositeOperation = 'lighter';
      var len = Math.sqrt((this.p2.x - this.p1.x) * (this.p2.x - this.p1.x) + (this.p2.y - this.p1.y) * (this.p2.y - this.p1.y));

      var accuracy = Math.max(-Math.floor(Math.log10(this.scene.simulator.totalTruncation)), 0);
      if (this.scene.simulator.totalTruncation > 0 && accuracy <= 2) {
        var str1 = "P=" + this.power.toFixed(accuracy) + "±" + this.scene.simulator.totalTruncation.toFixed(accuracy);
        var str2 = "F⊥=" + this.normal.toFixed(accuracy) + "±" + this.scene.simulator.totalTruncation.toFixed(accuracy);
        var str3 = "F∥=" + this.shear.toFixed(accuracy) + "±" + this.scene.simulator.totalTruncation.toFixed(accuracy);
      } else {
        var str1 = "P=" + this.power.toFixed(2);
        var str2 = "F⊥=" + this.normal.toFixed(2);
        var str3 = "F∥=" + this.shear.toFixed(2);
      }

      ctx.font = (16 * ls) + 'px Arial';
      ctx.textAlign = 'right';
      ctx.textBaseline = 'top';
      ctx.fillStyle = isHovered ? 'cyan' : ('rgb(192,192,192)');
      ctx.fillText(str1, this.p2.x, this.p2.y);
      ctx.fillText(str2, this.p2.x, this.p2.y + 20 * ls);
      ctx.fillText(str3, this.p2.x, this.p2.y + 40 * ls);
      ctx.globalCompositeOperation = 'source-over';

      if (this.irradMap && this.binData) {
        // Define the unit vector of the x-axis of the plot (parallel to obj) and the y-axis of the plot (perpendicular to obj)
        var len = Math.sqrt((this.p2.x - this.p1.x) * (this.p2.x - this.p1.x) + (this.p2.y - this.p1.y) * (this.p2.y - this.p1.y));
        var ux = (this.p2.x - this.p1.x) / len;
        var uy = (this.p2.y - this.p1.y) / len;
        var vx = uy;
        var vy = -ux;

        // Draw the irradiance map
        ctx.lineWidth = 1 * ls;
        ctx.strokeStyle = isHovered ? 'cyan' : ('rgb(255,255,255)');
        ctx.fillStyle = 'blue';
        ctx.beginPath();
        ctx.moveTo(this.p1.x, this.p1.y);
        for (var i = 0; i < this.binData.length; i++) {
          ctx.lineTo(this.p1.x + ux * i * this.binSize + vx * this.binData[i] / this.binSize * 20 * ls * ls, this.p1.y + uy * i * this.binSize + vy * this.binData[i] / this.binSize * 20 * ls * ls);
          ctx.lineTo(this.p1.x + ux * (i + 1) * this.binSize + vx * this.binData[i] / this.binSize * 20 * ls * ls, this.p1.y + uy * (i + 1) * this.binSize + vy * this.binData[i] / this.binSize * 20 * ls * ls);
        }
        ctx.lineTo(this.p2.x, this.p2.y);
        ctx.fill();
        ctx.stroke();
      }
    }
  }

  onSimulationStart() {
    this.power = 0;
    this.normal = 0;
    this.shear = 0;

    if (this.irradMap) {
      var binSize = this.binSize;
      var binNum = Math.ceil(Math.sqrt((this.p2.x - this.p1.x) * (this.p2.x - this.p1.x) + (this.p2.y - this.p1.y) * (this.p2.y - this.p1.y)) / binSize);
      var binData = [];
      for (var i = 0; i < binNum; i++) {
        binData[i] = 0;
      }
      this.binData = binData;
    }
  }

  checkRayIntersects(ray) {
    return this.checkRayIntersectsShape(ray);
  }

  onRayIncident(ray, rayIndex, incidentPoint) {
    var rcrosss = (ray.p2.x - ray.p1.x) * (this.p2.y - this.p1.y) - (ray.p2.y - ray.p1.y) * (this.p2.x - this.p1.x);
    var sint = rcrosss / Math.sqrt((ray.p2.x - ray.p1.x) * (ray.p2.x - ray.p1.x) + (ray.p2.y - ray.p1.y) * (ray.p2.y - ray.p1.y)) / Math.sqrt((this.p2.x - this.p1.x) * (this.p2.x - this.p1.x) + (this.p2.y - this.p1.y) * (this.p2.y - this.p1.y));
    var cost = ((ray.p2.x - ray.p1.x) * (this.p2.x - this.p1.x) + (ray.p2.y - ray.p1.y) * (this.p2.y - this.p1.y)) / Math.sqrt((ray.p2.x - ray.p1.x) * (ray.p2.x - ray.p1.x) + (ray.p2.y - ray.p1.y) * (ray.p2.y - ray.p1.y)) / Math.sqrt((this.p2.x - this.p1.x) * (this.p2.x - this.p1.x) + (this.p2.y - this.p1.y) * (this.p2.y - this.p1.y));
    ray.p2 = geometry.point(incidentPoint.x + ray.p2.x - ray.p1.x, incidentPoint.y + ray.p2.y - ray.p1.y);
    ray.p1 = geometry.point(incidentPoint.x, incidentPoint.y);

    this.power += Math.sign(rcrosss) * (ray.brightness_s + ray.brightness_p);
    this.normal += Math.sign(rcrosss) * sint * (ray.brightness_s + ray.brightness_p);
    this.shear -= Math.sign(rcrosss) * cost * (ray.brightness_s + ray.brightness_p);

    if (this.irradMap && this.binData) {
      var binSize = this.binSize;
      var binNum = Math.ceil(Math.sqrt((this.p2.x - this.p1.x) * (this.p2.x - this.p1.x) + (this.p2.y - this.p1.y) * (this.p2.y - this.p1.y)) / binSize);
      var binIndex = Math.floor(Math.sqrt((incidentPoint.x - this.p1.x) * (incidentPoint.x - this.p1.x) + (incidentPoint.y - this.p1.y) * (incidentPoint.y - this.p1.y)) / binSize);
      this.binData[binIndex] += Math.sign(rcrosss) * (ray.brightness_s + ray.brightness_p);
    }
  }
};

export default Detector;