src/engine/tools2d/ToolArea.js (396 lines of code) (raw):
/*
* Copyright 2021 EPAM Systems, Inc. (https://www.epam.com/)
* SPDX-License-Identifier: Apache-2.0
*/
/**
* @fileOverview ToolArea
* @author Epam
* @version 1.0.0
*/
// **********************************************
// Imports
// **********************************************
import Modes2d from '../../store/Modes2d';
import PointerChecker from '../utils/PointerChecker';
import ToolDistance from './ToolDistance';
// **********************************************
// Class
// **********************************************
class ToolArea {
constructor(objGra) {
this.m_objGraphics2d = objGra;
this.m_wScreen = 0;
this.m_hScreen = 0;
this.m_areas = [];
this.m_inCreateMode = false;
this.m_objSelfIntersect = null;
this.m_xPixelSize = 0;
this.m_yPixelSize = 0;
this.m_objEdit = null;
this.store = null;
this.onMouseDown = this.onMouseDown.bind(this);
this.onMouseUp = this.onMouseUp.bind(this);
this.onMouseMove = this.onMouseMove.bind(this);
this.render = this.render.bind(this);
}
setScreenDim(wScr, hScr) {
this.m_wScreen = wScr;
this.m_hScreen = hScr;
}
setPixelSize(xs, ys) {
this.m_xPixelSize = xs;
this.m_yPixelSize = ys;
}
clear() {
this.m_areas = [];
this.m_inCreateMode = false;
this.m_objSelfIntersect = null;
}
/**
* Determine intersection with points in areas set.
* Input - screen coordinates of pick point
* Output - volume coordinate
*
* @param {object} vScr - screen coordinates of poick
* @param {object} store - global store
*/
getEditPoint(vScr, store) {
this.store = store;
const numAreas = this.m_areas.length;
for (let i = 0; i < numAreas; i++) {
const objArea = this.m_areas[i];
const lastPoint = {};
for (let j = 0; j < objArea.m_points.length; j++) {
const vScrProj_S = ToolDistance.textureToScreen(
objArea.m_points[j].x,
objArea.m_points[j].y,
this.m_wScreen,
this.m_hScreen,
store
);
if (!j) {
lastPoint.x = vScrProj_S.x;
lastPoint.y = vScrProj_S.y;
}
const vScrProj_E =
j < objArea.m_points.length - 1
? ToolDistance.textureToScreen(objArea.m_points[j + 1].x, objArea.m_points[j + 1].y, this.m_wScreen, this.m_hScreen, store)
: lastPoint;
if (PointerChecker.isPointerOnLine(vScrProj_S, vScrProj_E, vScr)) {
this.m_objEdit = objArea;
return objArea.m_points[j];
}
} // for (j) all point in area
} // for (i) all areas
return null;
}
/**
* Move edited point into new pos
*
* @param {object} vVolOld
* @param {object} vVolNew
*/
moveEditPoint(vVolOld, vVolNew) {
const x = vVolOld.x;
const y = vVolOld.y;
vVolOld.x = vVolNew.x;
vVolOld.y = vVolNew.y;
const hasInters = ToolArea.hasSelfIntersection(this.m_objEdit.m_points);
if (hasInters) {
vVolOld.x = x;
vVolOld.y = y;
}
// update line len
const store = this.store;
this.m_objEdit.m_area = this.getPolyArea(this.m_objEdit.m_points, store);
}
/**
* Remove highlighted object
*/
deleteObject() {
if (this.m_objEdit != null) {
const ind = this.m_areas.indexOf(this.m_objEdit);
if (ind >= 0) {
this.m_areas.splice(ind, 1);
}
}
}
getDistMm(vs, ve) {
const dx = vs.x - ve.x;
const dy = vs.y - ve.y;
const dist = Math.sqrt(dx * dx * this.m_xPixelSize * this.m_xPixelSize + dy * dy * this.m_yPixelSize * this.m_yPixelSize);
return dist;
}
/**
* Get lines intersection in 2d
*
* @param {object} v0
* @param {object} v1
* @param {object} v2
* @param {object} v3
* @return {object} point (object with x,y) of intersection or null
*/
static getLineIntersection(v0, v1, v2, v3) {
const v23 = {
x: v3.x - v2.x,
y: v3.y - v2.y,
};
const n23 = {
x: -v23.y,
y: v23.x,
};
const v01 = {
x: v1.x - v0.x,
y: v1.y - v0.y,
};
const v02 = {
x: v2.x - v0.x,
y: v2.y - v0.y,
};
const dotUp = v02.x * n23.x + v02.y * n23.y;
const dotDn = v01.x * n23.x + v01.y * n23.y;
const TOO_SMALL = 1.0e-6;
if (Math.abs(dotDn) < TOO_SMALL) {
return null;
}
const t01 = dotUp / dotDn; // should be in [0..1]
if (t01 < 0.0 || t01 > 1.0) {
return null;
}
// check intersect from 2nd line
const v20 = {
x: -v02.x,
y: -v02.y,
};
const n01 = {
x: -v01.y,
y: v01.x,
};
const dotProdDn = v23.x * n01.x + v23.y * n01.y;
if (Math.abs(dotProdDn) < TOO_SMALL) {
return null;
}
const dotProdUp = v20.x * n01.x + v20.y * n01.y;
const t23 = dotProdUp / dotProdDn; // should be in [0..1]
if (t23 < 0.0 || t23 > 1.0) {
return null;
}
const vIntersection = {
x: v0.x + v01.x * t01,
y: v0.y + v01.y * t01,
};
return vIntersection;
}
static hasSelfIntersection(points) {
let i, j;
for (i = 0; i < points.length; i++) {
const iNext = i + 1 < points.length ? i + 1 : 0;
const vA = points[i];
const vB = points[iNext];
for (j = 0; j < points.length; j++) {
const jNext = j + 1 < points.length ? j + 1 : 0;
if (i !== j && i !== jNext && iNext !== j && iNext !== jNext) {
const vC = points[j];
const vD = points[jNext];
const vIntersect = ToolArea.getLineIntersection(vA, vB, vC, vD);
if (vIntersect !== null) {
return true;
}
} // if not same index
} // for (j)
} // for (i)
return false;
}
static getSelfIntersectPoint(points) {
const numPoints = points.length;
if (numPoints <= 3) {
return null;
}
const vLast0 = points[numPoints - 2];
const vLast1 = points[numPoints - 1];
const limPoints = numPoints - 3;
for (let i = 0; i < limPoints; i++) {
const vLine0 = points[i + 0];
const vLine1 = points[i + 1];
const vIntersect = ToolArea.getLineIntersection(vLast0, vLast1, vLine0, vLine1);
if (vIntersect !== null) {
const objInter = {
m_vIntersection: vIntersect,
m_lineIndex: i,
};
return objInter;
}
} // for (i) checked points in poly
// check dist to first
const dx = vLast1.x - points[0].x;
const dy = vLast1.y - points[0].y;
const MIN_DIST = 2.5;
const MIN_DIST_SQ = MIN_DIST * MIN_DIST;
if (dx * dx + dy * dy <= MIN_DIST_SQ) {
const vInter = {
x: vLast1.x,
y: vLast1.y,
};
const objInter = {
m_vIntersection: vInter,
m_lineIndex: 0,
};
return objInter;
}
return null;
}
/**
*
* @param {object} vNew - new added point
* @param {array} points - array of polygon's points
* @return true, if polygon is finished
*/
addPointToPolygon(vNew, points) {
const numPoints = points.length;
if (numPoints === 0) {
points.push(vNew);
return false;
}
const objInter = this.m_objSelfIntersect;
if (objInter !== null) {
const polyNew = [];
polyNew.push(objInter.m_vIntersection);
for (let i = objInter.m_lineIndex + 1; i < numPoints - 1; i++) {
const vAdd = {
x: points[i].x,
y: points[i].y,
};
polyNew.push(vAdd);
}
// copy to points
points.length = polyNew.length;
for (let i = 0; i < polyNew.length; i++) {
points[i].x = polyNew[i].x;
points[i].y = polyNew[i].y;
}
this.m_objSelfIntersect = null;
return true;
} // if has some intersect point
// just add point to poly
points.push(vNew);
return false;
}
/**
*
* @param {array} points - array of points (x,y) props
*/
getPolyArea(points, store) {
const volSet = store.volumeSet;
const volIndex = store.volumeIndex;
const vol = volSet.getVolume(volIndex);
const xSize = vol.m_boxSize.x;
const ySize = vol.m_boxSize.y;
const zSize = vol.m_boxSize.z;
const mode2d = store.mode2d;
const xDim = vol.m_xDim;
const yDim = vol.m_yDim;
const zDim = vol.m_zDim;
// const zDim = vol.m_zDim;
let xScale = 0.0,
yScale = 0.0;
if (mode2d === Modes2d.TRANSVERSE) {
// z const
xScale = xSize / xDim;
yScale = ySize / yDim;
} else if (mode2d === Modes2d.SAGGITAL) {
// x const
xScale = ySize / xDim;
// yScale = zSize / yDim;
yScale = zSize / zDim;
} else {
// y const
xScale = xSize / xDim;
// yScale = zSize / yDim;
yScale = zSize / zDim;
}
let area = 0.0;
const numPoints = points.length;
let j = numPoints - 1;
for (let i = 0; i < numPoints; i++) {
const vi = points[i];
const vj = points[j];
let areaTri = (vj.x + vi.x) * (vj.y - vi.y) * xScale * yScale;
areaTri = areaTri > 0.0 ? areaTri : -areaTri;
area += areaTri;
j = i;
}
return area * 0.5;
}
/**
* When mouse button is pressed
*
* @param {number} xScr - x screen coordinate
* @param {number} yScr - y screen coordinate
* @param {object} store - global store with all app parameters
*/
onMouseDown(xScr, yScr, store) {
const vTex = ToolDistance.screenToTexture(xScr, yScr, this.m_wScreen, this.m_hScreen, store);
if (!this.m_inCreateMode) {
this.m_inCreateMode = true;
// create new area
const v0 = {
x: vTex.x,
y: vTex.y,
};
const v1 = {
x: vTex.x,
y: vTex.y,
};
const objArea = {
m_isClosed: false,
m_points: [],
m_area: 0.0,
};
objArea.m_points.push(v0);
objArea.m_points.push(v1);
this.m_areas.push(objArea);
} else {
// add new point to polygon
const vNew = {
x: vTex.x,
y: vTex.y,
};
const numAreas = this.m_areas.length;
const objArea = this.m_areas[numAreas - 1];
const isFinished = this.addPointToPolygon(vNew, objArea.m_points);
if (isFinished) {
objArea.m_isClosed = true;
objArea.m_area = this.getPolyArea(objArea.m_points, store);
console.log(`ToolArea. area = ${objArea.m_area}`);
// prepare to new added polygon
this.m_inCreateMode = false;
}
}
}
/**
* When mouse is moved
*
* @param {number} xScr - x screen coordinate
* @param {number} yScr - y screen coordinate
* @param {object} store - global store with all app parameters
*/
onMouseMove(xScr, yScr, store) {
if (!this.m_inCreateMode) {
return;
}
const vTex = ToolDistance.screenToTexture(xScr, yScr, this.m_wScreen, this.m_hScreen, store);
// modify last point in last area
const numAreas = this.m_areas.length;
const objArea = this.m_areas[numAreas - 1];
const numPoints = objArea.m_points.length;
objArea.m_points[numPoints - 1].x = vTex.x;
objArea.m_points[numPoints - 1].y = vTex.y;
const pt = ToolArea.getSelfIntersectPoint(objArea.m_points);
this.m_objSelfIntersect = pt;
this.m_objGraphics2d.forceUpdate();
}
onMouseUp() {
// ommited args: xScr, yScr, store
}
/**
* Render all areas on screen in 2d mode
*
* @param {object} ctx - html5 canvas context
* @param {object} store - global store with app parameters
*/
render(ctx, store) {
ctx.lineWidth = 2;
ctx.strokeStyle = 'yellow';
ctx.fillStyle = 'white';
const FONT_SZ = 16;
ctx.font = FONT_SZ.toString() + 'px Arial';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
// render possible self intersect point
if (this.m_objSelfIntersect !== null) {
const vTex = this.m_objSelfIntersect.m_vIntersection;
const vScr = ToolDistance.textureToScreen(vTex.x, vTex.y, this.m_wScreen, this.m_hScreen, store);
ctx.strokeStyle = 'red';
ctx.beginPath();
const RAD_CIRCLE = 5.0;
const M_PI = 3.1415926535;
const TWO = 2;
ctx.arc(vScr.x, vScr.y, RAD_CIRCLE, 0.0, TWO * M_PI);
ctx.stroke();
}
ctx.strokeStyle = 'yellow';
const numAreas = this.m_areas.length;
for (let a = 0; a < numAreas; a++) {
const objArea = this.m_areas[a];
const isClosed = objArea.m_isClosed;
const numPoints = isClosed ? objArea.m_points.length + 1 : objArea.m_points.length;
// calc area centroid in screen
let xScrCenter = 0.0;
let yScrCenter = 0.0;
ctx.beginPath();
for (let i = 0; i < numPoints; i++) {
const iPoly = i < objArea.m_points.length ? i : 0;
const vTex0 = objArea.m_points[iPoly];
const vScr = ToolDistance.textureToScreen(vTex0.x, vTex0.y, this.m_wScreen, this.m_hScreen, store);
if (i === 0) {
ctx.moveTo(vScr.x, vScr.y);
} else {
ctx.lineTo(vScr.x, vScr.y);
}
xScrCenter += vScr.x;
yScrCenter += vScr.y;
} // for (i) all points in poly
ctx.stroke();
if (numPoints > 0) {
xScrCenter /= numPoints;
yScrCenter /= numPoints;
}
// draw area
if (isClosed) {
const strMsg = objArea.m_area.toFixed(2) + ' mm^2';
ctx.fillText(strMsg, xScrCenter, yScrCenter);
} // if this poly closed
} // for (a) all polys
}
} // end class
export default ToolArea;