UNPKG

@edsilv/ami.js

Version:

<p align="center"> <img src="https://user-images.githubusercontent.com/214063/46479857-4cd66e80-c7f0-11e8-9585-5748409c9490.png" width="60%"> </p>

606 lines (480 loc) 17.7 kB
import { widgetsBase } from './widgets.base'; import { widgetsHandle as widgetsHandleFactory } from './widgets.handle'; import CoreUtils from '../core/core.utils'; /** * @module widgets/freehand */ const widgetsFreehand = (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 = 'Freehand'; // incoming parameters (optional: frameIndex, worldPosition) this._stack = params.stack; // required this._calibrationFactor = params.calibrationFactor || null; // outgoing values this._area = null; this._units = !this._calibrationFactor && !params.stack.frame[params.frameIndex].pixelSpacing ? 'units' : 'cm²'; this._initialized = false; // set to true onEnd if number of handles > 2 this._moving = false; this._domHovered = false; // mesh stuff this._material = null; this._geometry = null; this._mesh = null; // dom stuff this._lines = []; this._label = null; // add handles this._handles = []; const WidgetsHandle = widgetsHandleFactory(three); let handle = new WidgetsHandle(targetMesh, controls, params); this.add(handle); this._handles.push(handle); this._moveHandle = new WidgetsHandle(targetMesh, controls, params); this.add(this._moveHandle); this._moveHandle.hide(); this.onMove = this.onMove.bind(this); this.onHover = this.onHover.bind(this); this.create(); this.addEventListeners(); } addEventListeners() { this._container.addEventListener('wheel', this.onMove); this._label.addEventListener('mouseenter', this.onHover); this._label.addEventListener('mouseleave', this.onHover); } removeEventListeners() { this._container.removeEventListener('wheel', this.onMove); this._label.removeEventListener('mouseenter', this.onHover); this._label.removeEventListener('mouseleave', this.onHover); } onHover(evt) { if (evt) { this.hoverDom(evt); } this.hoverMesh(); let hovered = false; this._handles.forEach(elem => (hovered = hovered || elem.hovered)); this._hovered = hovered || this._domHovered; this._container.style.cursor = this._hovered ? 'pointer' : 'default'; } hoverMesh() { // check raycast intersection, if we want to hover on mesh instead of just css } hoverDom(evt) { this._domHovered = evt.type === 'mouseenter'; } onStart(evt) { let active = false; this._moveHandle.onMove(evt, true); this._handles.forEach(elem => { elem.onStart(evt); active = active || elem.active; }); this._active = active || this._domHovered; if (this._domHovered && this._initialized) { this._moving = true; this._controls.enabled = false; } this.update(); } onMove(evt) { let hovered = false; if (this.active) { this._dragged = true; if (!this._initialized) { this._handles[this._handles.length - 1].hovered = false; this._handles[this._handles.length - 1].active = false; this._handles[this._handles.length - 1].tracking = false; const WidgetsHandle = widgetsHandleFactory(three); let handle = new WidgetsHandle(this._targetMesh, this._controls, this._params); handle.hovered = true; handle.active = true; handle.tracking = true; this.add(handle); this._handles.push(handle); this.createLine(); } else { const prevPosition = this._moveHandle.worldPosition.clone(); this._moveHandle.onMove(evt, true); if (this._mesh) { this.remove(this._mesh); } this.updateDOMContent(true); if (this._moving) { this._handles.forEach(handle => { handle.worldPosition.add(this._moveHandle.worldPosition.clone().sub(prevPosition)); }); } } } this._handles.forEach(elem => { elem.onMove(evt); hovered = hovered || elem.hovered; }); this._hovered = hovered || this._domHovered; this._container.style.cursor = this._hovered ? 'pointer' : 'default'; if (this.active && this._handles.length > 2) { this.pushPopHandle(); } this.update(); } onEnd() { if (this._handles.length < 3) { return; } let active = false; this._handles.slice(0, -1).forEach(elem => { elem.onEnd(); active = active || elem.active; }); // Last Handle if (this._dragged || !this._handles[this._handles.length - 1].tracking) { this._handles[this._handles.length - 1].tracking = false; this._handles[this._handles.length - 1].onEnd(); } else { this._handles[this._handles.length - 1].tracking = false; } if (this._lines.length < this._handles.length) { this.createLine(); } if (this._dragged || !this._initialized) { this.updateMesh(); this.updateDOMContent(); } if (!this._dragged && this._active) { this._selected = !this._selected; // change state if there was no dragging this._handles.forEach(elem => (elem.selected = this._selected)); } this._active = active || this._handles[this._handles.length - 1].active; this._dragged = false; this._moving = false; this._initialized = true; this.update(); } create() { this.createMaterial(); this.createDOM(); } createMaterial() { this._material = new three.MeshBasicMaterial({ side: three.DoubleSide }); this._material.transparent = true; this._material.opacity = 0.2; } createDOM() { this._label = document.createElement('div'); this._label.className = 'widgets-label'; // measurements const measurementsContainer = document.createElement('div'); // Mean / SD let meanSDContainer = document.createElement('div'); meanSDContainer.className = 'mean-sd'; measurementsContainer.appendChild(meanSDContainer); // Max / Min let maxMinContainer = document.createElement('div'); maxMinContainer.className = 'max-min'; measurementsContainer.appendChild(maxMinContainer); // Area let areaContainer = document.createElement('div'); areaContainer.className = 'area'; measurementsContainer.appendChild(areaContainer); this._label.appendChild(measurementsContainer); this._container.appendChild(this._label); this.updateDOMColor(); } createLine() { const line = document.createElement('div'); line.className = 'widgets-line'; line.addEventListener('mouseenter', this.onHover); line.addEventListener('mouseleave', this.onHover); this._lines.push(line); this._container.appendChild(line); } hideDOM() { this._handles.forEach(elem => elem.hideDOM()); this._lines.forEach(elem => (elem.style.display = 'none')); this._label.style.display = 'none'; } showDOM() { this._handles.forEach(elem => elem.showDOM()); this._lines.forEach(elem => (elem.style.display = '')); this._label.style.display = ''; } update() { this.updateColor(); // update handles this._handles.forEach(elem => elem.update()); // mesh stuff this.updateMeshColor(); this.updateMeshPosition(); // DOM stuff this.updateDOMColor(); this.updateDOMPosition(); } updateMesh() { if (this._mesh) { this.remove(this._mesh); } let points = []; this._handles.forEach(elem => points.push(elem.worldPosition)); let center = CoreUtils.centerOfMass(points); // direction from first point to center let referenceDirection = new three.Vector3().subVectors(points[0], center).normalize(); let direction = new three.Vector3().crossVectors( new three.Vector3().subVectors(points[0], center), // side 1 new three.Vector3().subVectors(points[1], center) // side 2 ); let base = new three.Vector3().crossVectors(referenceDirection, direction).normalize(); let orderedpoints = []; // other lines // if inter, return location + angle for (let j = 0; j < points.length; j++) { let point = new three.Vector3(points[j].x, points[j].y, points[j].z); point.direction = new three.Vector3().subVectors(points[j], center).normalize(); let x = referenceDirection.dot(point.direction); let y = base.dot(point.direction); point.xy = { x, y }; point.angle = Math.atan2(y, x) * (180 / Math.PI); orderedpoints.push(point); } // override to catch console.warn "THREE.ShapeUtils: Unable to triangulate polygon! in triangulate()" this._shapeWarn = false; const oldWarn = console.warn; console.warn = function(...rest) { if (rest[0] === 'three.ShapeUtils: Unable to triangulate polygon! in triangulate()') { this._shapeWarn = true; } return oldWarn.apply(console, rest); }.bind(this); // create the shape let shape = new three.Shape(); // move to first point! shape.moveTo(orderedpoints[0].xy.x, orderedpoints[0].xy.y); // loop through all points! for (let l = 1; l < orderedpoints.length; l++) { // project each on plane! shape.lineTo(orderedpoints[l].xy.x, orderedpoints[l].xy.y); } // close the shape! shape.lineTo(orderedpoints[0].xy.x, orderedpoints[0].xy.y); this._geometry = new three.ShapeGeometry(shape); console.warn = oldWarn; this._geometry.vertices = orderedpoints; this._geometry.verticesNeedUpdate = true; this._geometry.elementsNeedUpdate = true; this.updateMeshColor(); this._mesh = new three.Mesh(this._geometry, this._material); this._mesh.visible = true; this.add(this._mesh); } updateMeshColor() { if (this._material) { this._material.color.set(this._color); } } updateMeshPosition() { if (this._geometry) { this._geometry.verticesNeedUpdate = true; } } isPointOnLine(pointA, pointB, pointToCheck) { let c = new three.Vector3(); c.crossVectors(pointA.clone().sub(pointToCheck), pointB.clone().sub(pointToCheck)); return !c.length(); } pushPopHandle() { let handle0 = this._handles[this._handles.length - 3]; let handle1 = this._handles[this._handles.length - 2]; let newhandle = this._handles[this._handles.length - 1]; let isOnLine = this.isPointOnLine( handle0.worldPosition, handle1.worldPosition, newhandle.worldPosition ); if (isOnLine || handle0.screenPosition.distanceTo(newhandle.screenPosition) < 25) { this.remove(handle1); handle1.free(); this._handles[this._handles.length - 2] = newhandle; this._handles.pop(); this._container.removeChild(this._lines.pop()); } return isOnLine; } updateDOMColor() { if (this._handles.length >= 2) { this._lines.forEach(elem => (elem.style.backgroundColor = this._color)); } this._label.style.borderColor = this._color; } updateDOMContent(clear) { const meanSDContainer = this._label.querySelector('.mean-sd'); const maxMinContainer = this._label.querySelector('.max-min'); const areaContainer = this._label.querySelector('.area'); if (clear) { meanSDContainer.innerHTML = ''; maxMinContainer.innerHTML = ''; areaContainer.innerHTML = ''; return; } const regions = this._stack.frame[this._params.frameIndex].ultrasoundRegions || []; this._area = CoreUtils.getGeometryArea(this._geometry); // this.getArea result is changed on dragging if (this._calibrationFactor) { this._area *= Math.pow(this._calibrationFactor, 2); } else if (regions && regions.length > 0 && this._stack.lps2IJK) { let same = true; let cRegion; let pRegion; this._handles.forEach(elem => { cRegion = this.getRegionByXY( regions, CoreUtils.worldToData(this._stack.lps2IJK, elem.worldPosition) ); if ( cRegion === null || regions[cRegion].unitsX !== 'cm' || (pRegion !== undefined && pRegion !== cRegion) ) { same = false; } pRegion = cRegion; }); if (same) { this._area *= Math.pow(regions[cRegion].deltaX, 2); this._units = 'cm²'; } else if (this._stack.frame[this._params.frameIndex].pixelSpacing) { this._area /= 100; this._units = 'cm²'; } else { this._units = 'units'; } } else if (this._units === 'cm²') { this._area /= 100; } let title = this._units === 'units' ? 'Calibration is required to display the area in cm². ' : ''; if (this._shapeWarn) { title += 'Values may be incorrect due to triangulation error.'; } if (title !== '' && !this._label.hasAttribute('title')) { this._label.setAttribute('title', title); this._label.style.color = this._colors.error; } else if (title === '' && this._label.hasAttribute('title')) { this._label.removeAttribute('title'); this._label.style.color = this._colors.text; } const roi = CoreUtils.getRoI(this._mesh, this._camera, this._stack); if (roi !== null) { meanSDContainer.innerHTML = `Mean: ${roi.mean.toFixed(1)} / SD: ${roi.sd.toFixed(1)}`; maxMinContainer.innerHTML = `Max: ${roi.max.toFixed()} / Min: ${roi.min.toFixed()}`; } else { meanSDContainer.innerHTML = ''; maxMinContainer.innerHTML = ''; } areaContainer.innerHTML = `Area: ${this._area.toFixed(2)} ${this._units}`; } updateDOMPosition() { if (this._handles.length < 2) { return; } // update lines and get coordinates of lowest handle let labelPosition = null; this._lines.forEach((elem, ind) => { const lineData = this.getLineData( this._handles[ind].screenPosition, this._handles[ind + 1 === this._handles.length ? 0 : ind + 1].screenPosition ); elem.style.transform = `translate3D(${lineData.transformX}px, ${lineData.transformY}px, 0) rotate(${lineData.transformAngle}rad)`; elem.style.width = lineData.length + 'px'; if (labelPosition === null || labelPosition.y < this._handles[ind].screenPosition.y) { labelPosition = this._handles[ind].screenPosition.clone(); } }); if (!this._initialized) { return; } // update label labelPosition.y += 15 + this._label.offsetHeight / 2; labelPosition = this.adjustLabelTransform(this._label, labelPosition); this._label.style.transform = `translate3D(${labelPosition.x}px, ${labelPosition.y}px, 0)`; } free() { this.removeEventListeners(); this._handles.forEach(h => { this.remove(h); h.free(); }); this._handles = []; this.remove(this._moveHandle); this._moveHandle.free(); this._moveHandle = null; this._lines.forEach(elem => { elem.removeEventListener('mouseenter', this.onHover); elem.removeEventListener('mouseleave', this.onHover); this._container.removeChild(elem); }); this._lines = []; this._container.removeChild(this._label); // mesh, geometry, material if (this._mesh) { this.remove(this._mesh); this._mesh.geometry.dispose(); this._mesh.geometry = null; this._mesh.material.dispose(); this._mesh.material = null; this._mesh = null; } if (this._geometry) { this._geometry.dispose(); this._geometry = null; } this._material.vertexShader = null; this._material.fragmentShader = null; this._material.uniforms = null; this._material.dispose(); this._material = null; this._stack = null; super.free(); } getMeasurements() { return { area: this._area, units: this._units, }; } get targetMesh() { return this._targetMesh; } set targetMesh(targetMesh) { this._targetMesh = targetMesh; this._handles.forEach(elem => (elem.targetMesh = targetMesh)); this._moveHandle.targetMesh = targetMesh; this.update(); } get worldPosition() { return this._worldPosition; } set worldPosition(worldPosition) { this._handles.forEach(elem => elem._worldPosition.copy(worldPosition)); this._worldPosition.copy(worldPosition); this.update(); } get calibrationFactor() { return this._calibrationFactor; } set calibrationFactor(calibrationFactor) { this._calibrationFactor = calibrationFactor; this._units = 'cm²'; this.update(); } }; }; export { widgetsFreehand }; export default widgetsFreehand();