src/widgets/widgets.pressureHalfTime.js
import { widgetsBase } from './widgets.base';
import { widgetsHandle as widgetsHandleFactory } from './widgets.handle';
import CoreUtils from '../core/core.utils';
/**
* @module widgets/pressureHalfTime
*/
const widgetsPressureHalfTime = (three = window.THREE) => {
if (three === undefined || three.Object3D === undefined) {
return null;
}
const Constructor = widgetsBase(three);
return class extends Constructor {
constructor(targetMesh, controls, params = {}) {
super(targetMesh, controls, params);
this._widgetType = 'PressureHalfTime';
// incoming parameters (required: lps2IJK, worldPosition)
this._regions = params.ultrasoundRegions || []; // required
if (this._regions.length < 1) {
throw new Error('Ultrasound regions should not be empty!');
}
// outgoing values
this._vMax = null; // Maximum Velocity (Vmax)
this._gMax = null; // Maximum Gradient (Gmax)
this._pht = null; // Pressure Half Time (PHT)
this._mva = null; // Mitral Valve Area (MVA)
this._dt = null; // Deceleration Time (DT)
this._ds = null; // Deceleration Slope (DS)
this._domHovered = false;
this._initialRegion = this.getRegionByXY(
this._regions,
CoreUtils.worldToData(params.lps2IJK, params.worldPosition)
);
if (this._initialRegion === null) {
throw new Error('Invalid initial UltraSound region!');
}
// mesh stuff
this._material = null;
this._geometry = null;
this._mesh = null;
// dom stuff
this._line = null;
this._label = null;
// add handles
this._handles = [];
const WidgetsHandle = widgetsHandleFactory(three);
let handle;
for (let i = 0; i < 2; i++) {
handle = new WidgetsHandle(targetMesh, controls, params);
this.add(handle);
this._handles.push(handle);
}
this._handles[1].active = true;
this._handles[1].tracking = true;
this._moveHandle = new WidgetsHandle(targetMesh, controls, params);
this.add(this._moveHandle);
this._handles.push(this._moveHandle);
this._moveHandle.hide();
this.create();
this.onMove = this.onMove.bind(this);
this.onHover = this.onHover.bind(this);
this.addEventListeners();
}
addEventListeners() {
this._container.addEventListener('wheel', this.onMove);
this._line.addEventListener('mouseenter', this.onHover);
this._line.addEventListener('mouseleave', this.onHover);
this._label.addEventListener('mouseenter', this.onHover);
this._label.addEventListener('mouseleave', this.onHover);
}
removeEventListeners() {
this._container.removeEventListener('wheel', this.onMove);
this._line.removeEventListener('mouseenter', this.onHover);
this._line.removeEventListener('mouseleave', this.onHover);
this._label.removeEventListener('mouseenter', this.onHover);
this._label.removeEventListener('mouseleave', this.onHover);
}
onHover(evt) {
if (evt) {
this.hoverDom(evt);
}
this.hoverMesh();
this._hovered = this._handles[0].hovered || this._handles[1].hovered || this._domHovered;
this._container.style.cursor = this._hovered ? 'pointer' : 'default';
}
hoverMesh() {
// check raycast intersection, do we want to hover on mesh or just css?
}
hoverDom(evt) {
this._domHovered = evt.type === 'mouseenter';
}
onStart(evt) {
this._moveHandle.onMove(evt, true);
this._handles[0].onStart(evt);
this._handles[1].onStart(evt);
this._active = this._handles[0].active || this._handles[1].active || this._domHovered;
if (this._domHovered) {
this._controls.enabled = false;
}
this.update();
}
onMove(evt) {
if (this._active) {
const prevPosition = this._moveHandle.worldPosition.clone();
this._moveHandle.onMove(evt, true);
const shift = this._moveHandle.worldPosition.clone().sub(prevPosition);
if (!this.isCorrectRegion(shift)) {
this._moveHandle.worldPosition.copy(prevPosition);
return;
}
if (!this._handles[0].active && !this._handles[1].active) {
this._handles.slice(0, -1).forEach(handle => {
handle.worldPosition.add(shift);
});
}
this._dragged = true;
} else {
this.onHover(null);
}
this._handles[0].onMove(evt);
this._handles[1].onMove(evt);
this.update();
}
onEnd() {
this._handles[0].onEnd(); // First Handle
if (
this._handles[1].tracking &&
this._handles[0].screenPosition.distanceTo(this._handles[1].screenPosition) < 10
) {
return;
}
if (!this._dragged && this._active && !this._handles[1].tracking) {
this._selected = !this._selected; // change state if there was no dragging
this._handles[0].selected = this._selected;
}
// Second Handle
if (this._dragged || !this._handles[1].tracking) {
this._handles[1].tracking = false;
this._handles[1].onEnd();
} else {
this._handles[1].tracking = false;
}
this._handles[1].selected = this._selected;
this._active = this._handles[0].active || this._handles[1].active;
this._dragged = false;
this.update();
}
isCorrectRegion(shift) {
const inActive = !(this._handles[0].active || this._handles[1].active);
let isCorrect = true;
if (this._handles[0].active || inActive) {
isCorrect = isCorrect && this.checkHandle(0, shift);
}
if (this._handles[1].active || inActive) {
isCorrect = isCorrect && this.checkHandle(1, shift);
}
return isCorrect;
}
checkHandle(index, shift) {
const region = this.getRegionByXY(
this._regions,
CoreUtils.worldToData(
this._params.lps2IJK,
this._handles[index].worldPosition.clone().add(shift)
)
);
return (
region !== null &&
region === this._initialRegion &&
this._regions[region].unitsY === 'cm/sec'
);
}
create() {
this.createMesh();
this.createDOM();
}
createMesh() {
// geometry
this._geometry = new three.Geometry();
this._geometry.vertices = [this._handles[0].worldPosition, this._handles[1].worldPosition];
// material
this._material = new three.LineBasicMaterial();
this.updateMeshColor();
// mesh
this._mesh = new three.Line(this._geometry, this._material);
this._mesh.visible = true;
this.add(this._mesh);
}
createDOM() {
this._line = document.createElement('div');
this._line.className = 'widgets-line';
this._container.appendChild(this._line);
this._label = document.createElement('div');
this._label.className = 'widgets-label';
const measurementsContainer = document.createElement('div');
['vmax', 'gmax', 'pht', 'mva', 'dt', 'ds'].forEach(name => {
const div = document.createElement('div');
div.className = name;
measurementsContainer.appendChild(div);
});
this._label.appendChild(measurementsContainer);
this._container.appendChild(this._label);
this.updateDOMColor();
}
hideDOM() {
this._line.style.display = 'none';
this._label.style.display = 'none';
this._handles.forEach(elem => elem.hideDOM());
}
showDOM() {
this._line.style.display = '';
this._label.style.display = '';
this._handles[0].showDOM();
this._handles[1].showDOM();
}
update() {
this.updateColor();
this._handles[0].update();
this._handles[1].update();
this.updateValues();
this.updateMeshColor();
this.updateMeshPosition();
this.updateDOM();
}
updateValues() {
const usPosition0 = this.getUsPoint(
this._regions,
CoreUtils.worldToData(this._params.lps2IJK, this._handles[0].worldPosition)
);
const usPosition1 = this.getUsPoint(
this._regions,
CoreUtils.worldToData(this._params.lps2IJK, this._handles[1].worldPosition)
);
const velocity0 = Math.abs(usPosition0.y / 100);
const velocity1 = Math.abs(usPosition1.y / 100);
const time0 = Math.abs(usPosition0.x);
const time1 = Math.abs(usPosition1.x);
const vMaxTime = this._vMax === velocity0 ? time0 : time1;
this._vMax = Math.max(velocity0, velocity1);
this._gMax = 4 * Math.pow(this._vMax, 2);
const phtVelocity = this._vMax / Math.sqrt(2);
const phtKoeff = (velocity0 - phtVelocity) / (velocity1 - phtVelocity);
const dtKoeff = velocity0 / velocity1;
this._pht =
phtKoeff === 1
? Number.POSITIVE_INFINITY
: Math.abs(vMaxTime - (time0 - phtKoeff * time1) / (1 - phtKoeff)) * 1000;
this._mva = 220 / this._pht;
this._dt =
dtKoeff === 1
? Number.POSITIVE_INFINITY
: Math.abs(vMaxTime - (time0 - dtKoeff * time1) / (1 - dtKoeff)) * 1000;
this._ds = this._dt === 0 ? Number.POSITIVE_INFINITY : (this._vMax / this._dt) * 1000;
}
updateMeshColor() {
if (this._material) {
this._material.color.set(this._color);
}
}
updateMeshPosition() {
if (this._geometry) {
this._geometry.verticesNeedUpdate = true;
}
}
updateDOM() {
this.updateDOMColor();
// update line
const lineData = this.getLineData(
this._handles[0].screenPosition,
this._handles[1].screenPosition
);
this._line.style.transform = `translate3D(${lineData.transformX}px, ${
lineData.transformY
}px, 0)
rotate(${lineData.transformAngle}rad)`;
this._line.style.width = lineData.length + 'px';
// update label
this._label.querySelector('.vmax').innerHTML = `Vmax: ${this._vMax.toFixed(2)} m/s`;
this._label.querySelector('.gmax').innerHTML = `Gmax: ${this._gMax.toFixed(2)} mmhg`;
this._label.querySelector('.pht').innerHTML = `PHT: ${this._pht.toFixed(1)} ms`;
this._label.querySelector('.mva').innerHTML = `MVA: ${this._mva.toFixed(2)} cm²`;
this._label.querySelector('.dt').innerHTML = `DT: ${this._dt.toFixed(1)} ms`;
this._label.querySelector('.ds').innerHTML = `DS: ${this._ds.toFixed(2)} m/s²`;
let angle = Math.abs(lineData.transformAngle);
if (angle > Math.PI / 2) {
angle = Math.PI - angle;
}
const labelPadding =
Math.tan(angle) < this._label.offsetHeight / this._label.offsetWidth
? this._label.offsetWidth / 2 / Math.cos(angle) + 15 // 5px for each handle + padding
: this._label.offsetHeight / 2 / Math.cos(Math.PI / 2 - angle) + 15;
const paddingVector = lineData.line.normalize().multiplyScalar(labelPadding);
const paddingPoint =
lineData.length > labelPadding * 2
? this._handles[1].screenPosition.clone().sub(paddingVector)
: this._handles[1].screenPosition.clone().add(paddingVector);
const transform = this.adjustLabelTransform(this._label, paddingPoint);
this._label.style.transform = `translate3D(${transform.x}px, ${transform.y}px, 0)`;
}
updateDOMColor() {
this._line.style.backgroundColor = this._color;
this._label.style.borderColor = this._color;
}
free() {
this.removeEventListeners();
this._handles.forEach(h => {
this.remove(h);
h.free();
});
this._handles = [];
this._container.removeChild(this._line);
this._container.removeChild(this._label);
// mesh, geometry, material
this.remove(this._mesh);
this._mesh.geometry.dispose();
this._mesh.geometry = null;
this._mesh.material.dispose();
this._mesh.material = null;
this._mesh = null;
this._geometry.dispose();
this._geometry = null;
this._material.vertexShader = null;
this._material.fragmentShader = null;
this._material.uniforms = null;
this._material.dispose();
this._material = null;
super.free();
}
getMeasurements() {
return {
vMax: this._vMax,
gMax: this._gMax,
pht: this._pht,
mva: this._mva,
dt: this._dt,
ds: this._ds,
};
}
get targetMesh() {
return this._targetMesh;
}
set targetMesh(targetMesh) {
this._targetMesh = targetMesh;
this._handles.forEach(elem => (elem.targetMesh = targetMesh));
this.update();
}
get worldPosition() {
return this._worldPosition;
}
set worldPosition(worldPosition) {
this._handles[0].worldPosition.copy(worldPosition);
this._handles[1].worldPosition.copy(worldPosition);
this._worldPosition.copy(worldPosition);
this.update();
}
};
};
export { widgetsPressureHalfTime };
export default widgetsPressureHalfTime();