UNPKG

@acransac/vtk.js

Version:

Visualization Toolkit for the Web

381 lines (305 loc) 11.2 kB
import macro from 'vtk.js/Sources/macro'; import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; import vtkInteractorObserver from 'vtk.js/Sources/Rendering/Core/InteractorObserver'; import * as vtkMath from 'vtk.js/Sources/Common/Core/Math'; import vtkPixelSpaceCallbackMapper from 'vtk.js/Sources/Rendering/Core/PixelSpaceCallbackMapper'; import vtkPointSource from 'vtk.js/Sources/Filters/Sources/PointSource'; import vtkHandleRepresentation from 'vtk.js/Sources/Interaction/Widgets/HandleRepresentation'; import { TextAlign, VerticalAlign, } from 'vtk.js/Sources/Interaction/Widgets/LabelRepresentation/Constants'; import { InteractionState } from 'vtk.js/Sources/Interaction/Widgets/HandleRepresentation/Constants'; // ---------------------------------------------------------------------------- // vtkLabelRepresentation methods // ---------------------------------------------------------------------------- function vtkLabelRepresentation(publicAPI, model) { // Set our className model.classHierarchy.push('vtkLabelRepresentation'); const superClass = { ...publicAPI }; function getCanvasPosition() { if (model.canvas) { // canvas left/bottom in CSS coords const dpr = window.devicePixelRatio; return { left: Number(model.canvas.style.left.split('px')[0]) * dpr, bottom: Number(model.canvas.style.bottom.split('px')[0]) * dpr, }; } return null; } publicAPI.buildRepresentation = () => { if (model.labelText !== null) { publicAPI.setLabelText(model.labelText); } publicAPI.modified(); }; publicAPI.getActors = () => [model.actor]; publicAPI.getNestedProps = () => publicAPI.getActors(); publicAPI.computeInteractionState = (pos) => { if (model.canvas) { const height = model.canvas.height; const width = model.canvas.width; const canvasPosition = getCanvasPosition(); // pos is in display coords if ( pos[0] >= canvasPosition.left && pos[0] <= canvasPosition.left + width && pos[1] >= canvasPosition.bottom && pos[1] <= canvasPosition.bottom + height ) { model.interactionState = InteractionState.SELECTING; } else { model.interactionState = InteractionState.OUTSIDE; } } return model.interactionState; }; publicAPI.startComplexWidgetInteraction = (startEventPos) => { // Record the current event position, and the rectilinear wipe position. model.startEventPosition[0] = startEventPos[0]; model.startEventPosition[1] = startEventPos[1]; model.startEventPosition[2] = 0.0; model.lastEventPosition[0] = startEventPos[0]; model.lastEventPosition[1] = startEventPos[1]; }; publicAPI.complexWidgetInteraction = (eventPos) => { if (model.interactionState === InteractionState.SELECTING) { const center = model.point.getCenter(); const displayCenter = vtkInteractorObserver.computeWorldToDisplay( model.renderer, center[0], center[1], center[2] ); const focalDepth = displayCenter[2]; const worldStartEventPosition = vtkInteractorObserver.computeDisplayToWorld( model.renderer, model.lastEventPosition[0], model.lastEventPosition[1], focalDepth ); const worldCurrentPosition = vtkInteractorObserver.computeDisplayToWorld( model.renderer, eventPos[0], eventPos[1], focalDepth ); publicAPI.moveFocus(worldStartEventPosition, worldCurrentPosition); model.lastEventPosition[0] = eventPos[0]; model.lastEventPosition[1] = eventPos[1]; publicAPI.modified(); } }; publicAPI.setWorldPosition = (position) => { model.point.setCenter(position); superClass.setWorldPosition(model.point.getCenter()); publicAPI.modified(); }; publicAPI.setDisplayPosition = (position) => { superClass.setDisplayPosition(position); publicAPI.setWorldPosition(model.worldPosition.getValue()); }; publicAPI.moveFocus = (start, end) => { const motionVector = []; vtkMath.subtract(end, start, motionVector); const focus = model.point.getCenter(); vtkMath.add(focus, motionVector, focus); publicAPI.setWorldPosition(focus); }; publicAPI.getBounds = () => { const center = model.point.getCenter(); const bounds = []; bounds[0] = model.placeFactor * (center[0] - 1); bounds[1] = model.placeFactor * (center[0] + 1); bounds[2] = model.placeFactor * (center[1] - 1); bounds[3] = model.placeFactor * (center[1] + 1); bounds[4] = model.placeFactor * (center[2] - 1); bounds[5] = model.placeFactor * (center[2] + 1); return bounds; }; publicAPI.setContainer = (container) => { if (model.container && model.container !== container) { model.container.removeChild(model.canvas); } if (model.container !== container) { model.container = container; if (model.container) { model.container.appendChild(model.canvas); } publicAPI.modified(); } }; publicAPI.setLabelStyle = (labelStyle) => { model.labelStyle = { ...model.labelStyle, ...labelStyle }; publicAPI.modified(); }; publicAPI.setSelectLabelStyle = (selectLabelStyle) => { model.selectLabelStyle = { ...model.selectLabelStyle, ...selectLabelStyle, }; publicAPI.modified(); }; publicAPI.computeTextDimensions = (text) => { const currentLabelStyle = model.highlight ? model.selectLabelStyle : model.labelStyle; const separatorRegExp = /\r?\n/; const separatorRes = separatorRegExp.exec(text); const separator = separatorRes !== null ? separatorRes[0] : null; const lines = text.split(separator); const lineSpace = currentLabelStyle.fontSize * (1 + currentLabelStyle.lineSpace); const padding = currentLabelStyle.fontSize / 4; const height = 2 * padding + currentLabelStyle.fontSize + (lines.length - 1) * lineSpace; const width = lines.reduce( (maxWidth, line) => Math.max(maxWidth, Math.round(model.context.measureText(line).width)), 0 ); return { width, height, lineSpace, padding, lines }; }; publicAPI.updateLabel = () => { if (model.context && model.canvas) { // Clear canvas model.context.clearRect(0, 0, model.canvas.width, model.canvas.height); // Render text if (model.actor.getVisibility()) { const currentLabelStyle = model.highlight ? model.selectLabelStyle : model.labelStyle; const { width, height, lineSpace, padding, lines, } = publicAPI.computeTextDimensions(model.labelText); model.canvas.height = Math.round(height); model.canvas.width = width + 2 * padding; // Update label style model.context.strokeStyle = currentLabelStyle.strokeColor; model.context.lineWidth = currentLabelStyle.strokeSize; model.context.fillStyle = currentLabelStyle.fontColor; model.context.font = `${currentLabelStyle.fontStyle} ${currentLabelStyle.fontSize}px ${currentLabelStyle.fontFamily}`; // Update canvas dimensions const x = padding; let y = currentLabelStyle.fontSize; // Add text lines.forEach((line) => { let offset = 0; if (model.textAlign === TextAlign.RIGHT) { offset = width - Math.round(model.context.measureText(line).width); } else if (model.textAlign === TextAlign.CENTER) { offset = 0.5 * (width - Math.round(model.context.measureText(line).width)); } model.context.strokeText(line, x + offset, y); model.context.fillText(line, x + offset, y); y += lineSpace; }); } } }; publicAPI.highlight = (highlight) => { model.highlight = highlight; publicAPI.modified(); }; publicAPI.getCanvasSize = () => { if (model.canvas) { return { height: model.canvas.height, width: model.canvas.width, }; } return null; }; } // ---------------------------------------------------------------------------- // Object factory // ---------------------------------------------------------------------------- function defaultValues(initialValues) { return { container: null, labelStyle: { fontColor: 'white', fontStyle: 'normal', fontSize: 15, fontFamily: 'Arial', strokeColor: 'black', strokeSize: 1, lineSpace: 0.2, }, labelText: '', textAlign: TextAlign.LEFT, verticalAlign: VerticalAlign.BOTTOM, selectLabelStyle: { fontColor: 'rgb(0, 255, 0)', fontStyle: 'normal', fontSize: 15, fontFamily: 'Arial', strokeColor: 'black', strokeSize: 1, lineSpace: 0.2, }, ...initialValues, }; } // ---------------------------------------------------------------------------- export function extend(publicAPI, model, initialValues = {}) { Object.assign(model, defaultValues(initialValues)); // Inheritance vtkHandleRepresentation.extend(publicAPI, model, initialValues); publicAPI.setPlaceFactor(1); // Canvas model.canvas = document.createElement('canvas'); model.canvas.style.position = 'absolute'; // Context model.context = model.canvas.getContext('2d'); // PixelSpaceCallbackMapper model.point = vtkPointSource.newInstance(); model.point.setNumberOfPoints(1); model.point.setRadius(0); model.mapper = vtkPixelSpaceCallbackMapper.newInstance(); model.mapper.setInputConnection(model.point.getOutputPort()); model.mapper.setCallback((coordList) => { if (model.canvas) { let yOffset = 0; if (model.verticalAlign === VerticalAlign.BOTTOM) { yOffset = -model.canvas.height; } else if (model.verticalAlign === VerticalAlign.CENTER) { yOffset = -0.5 * model.canvas.height; } // coordList[0] is in display coords const dpr = window.devicePixelRatio; model.canvas.style.left = `${Math.round(coordList[0][0]) / dpr}px`; model.canvas.style.bottom = `${ Math.round(coordList[0][1] + yOffset) / dpr }px`; publicAPI.modified(); } }); model.actor = vtkActor.newInstance(); model.actor.setMapper(model.mapper); model.actorVisibility = true; model.highlight = false; model.actor.onModified(() => { if (model.actorVisibility !== model.actor.getVisibility()) { model.actorVisibility = model.actor.getVisibility(); publicAPI.modified(); } }); publicAPI.onModified(() => { publicAPI.updateLabel(); }); macro.setGet(publicAPI, model, ['labelText', 'textAlign', 'verticalAlign']); macro.get(publicAPI, model, ['container', 'labelStyle']); // Object methods vtkLabelRepresentation(publicAPI, model); } // ---------------------------------------------------------------------------- export const newInstance = macro.newInstance(extend, 'vtkLabelRepresentation'); // ---------------------------------------------------------------------------- export default { newInstance, extend };