diff --git a/Sources/Interaction/Widgets/LineRepresentation/Constants.js b/Sources/Interaction/Widgets/LineRepresentation/Constants.js new file mode 100644 index 00000000000..9c765a33b99 --- /dev/null +++ b/Sources/Interaction/Widgets/LineRepresentation/Constants.js @@ -0,0 +1,21 @@ +export const State = { + OUTSIDE: 0, + ONP1: 1, + ONP2: 2, + TRANSLATINGP1: 3, + TRANSLATINGP2: 4, + ONLINE: 5, + SCALING: 6, +}; + +export const Restrict = { + NONE: 0, + X: 1, + Y: 2, + Z: 3, +}; + +export default { + State, + Restrict, +}; diff --git a/Sources/Interaction/Widgets/LineRepresentation/index.js b/Sources/Interaction/Widgets/LineRepresentation/index.js new file mode 100644 index 00000000000..0ad2fa33c78 --- /dev/null +++ b/Sources/Interaction/Widgets/LineRepresentation/index.js @@ -0,0 +1,496 @@ +import Constants from 'vtk.js/Sources/Interaction/Widgets/LineRepresentation/Constants'; +import macro from 'vtk.js/Sources/macro'; +import vtkActor from 'vtk.js/Sources/Rendering/Core/Actor'; +import vtkBox from 'vtk.js/Sources/Common/DataModel/Box'; +import vtkLine from 'vtk.js/Sources/Common/DataModel/Line'; +import vtkLineSource from 'vtk.js/Sources/Filters/Sources/LineSource'; +import vtkMapper from 'vtk.js/Sources/Rendering/Core/Mapper'; +import vtkProperty from 'vtk.js/Sources/Rendering/Core/Property'; +import vtkSphereHandleRepresentation from 'vtk.js/Sources/Interaction/Widgets/SphereHandleRepresentation'; +import vtkWidgetRepresentation from 'vtk.js/Sources/Interaction/Widgets/WidgetRepresentation'; +import { InteractionState } from '../HandleRepresentation/Constants'; + + +const { State, Restrict } = Constants; + +// ---------------------------------------------------------------------------- +// vtkLineRepresentation methods +// ---------------------------------------------------------------------------- + +function vtkLineRepresentation(publicAPI, model) { + // Set our className + model.classHierarchy.push('vtkLineRepresentation'); + + const superClass = Object.assign({}, publicAPI); + + publicAPI.setResolution = (res) => { + model.lineSource.setResolution(res); + }; + + publicAPI.setLineVisibility = (visibility) => { + model.lineActor.setVisibility(visibility); + }; + + publicAPI.setPoint1Visibility = (visibility) => { + model.point1Representation.getActors().setVisibility(visibility); + }; + + publicAPI.setPoint2Visibility = (visibility) => { + model.point2Representation.getActors().setVisibility(visibility); + }; + + publicAPI.getResolution = () => model.lineSource.getResolution(); + publicAPI.getPoint1WorldPosition = () => model.point1Representation.getWorldPosition(); + publicAPI.getPoint2WorldPosition = () => model.point2Representation.getWorldPosition(); + publicAPI.getPoint1DisplayPosition = () => model.point1Representation.getDisplayPosition(); + publicAPI.getPoint2DisplayPosition = () => model.point2Representation.getDisplayPosition(); + publicAPI.setPoint1WorldPosition = (pos) => { + model.point1Representation.setWorldPosition(pos); + model.lineSource.setPoint1(...pos); + }; + publicAPI.setPoint2WorldPosition = (pos) => { + model.point2Representation.setWorldPosition(pos); + model.lineSource.setPoint2(...pos); + }; + publicAPI.setPoint1DisplayPosition = (pos) => { + model.point1Representation.setDisplayPosition(pos); + const p = model.point1Representation.getWorldPosition(); + model.point1Representation.setWorldPosition(p); + }; + publicAPI.setPoint2DisplayPosition = (pos) => { + model.point2Representation.setDisplayPosition(pos); + const p = model.point2Representation.getWorldPosition(); + model.point2Representation.setWorldPosition(p); + }; + + publicAPI.setRenderer = (renderer) => { + model.point1Representation.setRenderer(renderer); + model.point2Representation.setRenderer(renderer); + superClass.setRenderer(renderer); + }; + + publicAPI.startComplexWidgetInteraction = (startEventPos) => { + // Store the start 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]; + model.lastEventPosition[2] = 0.0; + + model.startP1 = model.point1Representation.getWorldPosition(); + model.startP2 = model.point2Representation.getWorldPosition(); + + if (model.interactionState === State.SCALING) { + const dp1 = model.point1Representation.getDisplayPosition(); + const dp2 = model.point2Representation.getDisplayPosition(); + model.length = Math.sqrt(((dp1[0] - dp2[0]) * (dp1[0] - dp2[0])) + ((dp1[1] - dp2[1]) * (dp1[1] - dp2[1]))); + } + }; + + publicAPI.complexWidgetInteraction = (e) => { + if (model.interactionState === State.ONP1) { + if (model.restrictFlag !== 0) { + const x = model.point1Representation.getWorldPosition(); + for (let i = 0; i < 3; i++) { + x[i] = (model.restrictFlag === (i + 1)) ? x[i] : model.startP1[i]; + } + model.point1Representation.setWorldPosition(x); + } + } else if (model.interactionState === State.ONP2) { + if (model.restrictFlag !== 0) { + const x = model.point2Representation.getWorldPosition(); + for (let i = 0; i < 3; i++) { + x[i] = (model.restrictFlag === (i + 1)) ? x[i] : model.startP2[i]; + } + model.point2Representation.setWorldPosition(x); + } + } else if (model.interactionState === State.ONLINE) { + // TODO + } else if (model.interactionState === State.SCALING) { + // TODO + } else if (model.interactionState === State.TRANSLATINGP1) { + const x = model.point1Representation.getWorldPosition(); + const p2 = []; + for (let i = 0; i < 3; i++) { + p2[i] = model.startP2[i] + (x[i] - model.startP1[i]); + } + model.point1Representation.setWorldPosition(p2); + } else if (model.interactionState === State.TRANSLATINGP2) { + const x = model.point2Representation.getWorldPosition(); + const p2 = []; + for (let i = 0; i < 3; i++) { + p2[i] = model.startP1[i] + (x[i] - model.startP2[i]); + } + model.point2Representation.setWorldPosition(p2); + } + + model.lastEventPosition[0] = e[0]; + model.lastEventPosition[1] = e[1]; + model.lastEventPosition[2] = 0.0; + publicAPI.modified(); + }; + + publicAPI.placeWidget = (...bounds) => { + let boundsArray = []; + + if (Array.isArray(bounds[0])) { + boundsArray = bounds[0]; + } else { + for (let i = 0; i < bounds.length; i++) { + boundsArray.push(bounds[i]); + } + } + + if (boundsArray.length !== 6) { + return; + } + + const placeFactorTemp = model.placeFactor; + model.placeFactor = 1.0; + const newBounds = []; + const center = []; + publicAPI.adjustBounds(boundsArray, newBounds, center); + model.placeFactor = placeFactorTemp; + + for (let i = 0; i < 6; i++) { + model.initialBounds[i] = newBounds[i]; + } + model.initialLength = Math.sqrt( + ((newBounds[1] - newBounds[0]) * (newBounds[1] - newBounds[0])) + + ((newBounds[3] - newBounds[2]) * (newBounds[3] - newBounds[2])) + + ((newBounds[5] - newBounds[4]) * (newBounds[5] - newBounds[4]))); + + // When PlaceWidget() is invoked, the widget orientation is preserved, but it + // is allowed to translate and scale. This means it is centered in the + // bounding box, and the representation scales itself to intersect the sides + // of the bounding box. Thus we have to determine where Point1 and Point2 + // intersect the bounding box. + const p1 = model.lineSource.getPoint1(); + const p2 = model.lineSource.getPoint2(); + + const r = [ + model.initialLength * (p1[0] - p2[0]), + model.initialLength * (p1[1] - p2[1]), + model.initialLength * (p1[2] - p2[2]), + ]; + const o = [ + center[0] - r[0], + center[1] - r[1], + center[2] - r[2], + ]; + + const placedP1 = []; + const t = []; + vtkBox.intersectBox(boundsArray, o, r, placedP1, t); + publicAPI.setPoint1WorldPosition(placedP1); + + r[0] = model.initialLength * (p2[0] - p1[0]); + r[1] = model.initialLength * (p2[1] - p1[1]); + r[2] = model.initialLength * (p2[2] - p1[2]); + o[0] = center[0] - r[0]; + o[1] = center[1] - r[1]; + o[2] = center[2] - r[2]; + const placedP2 = []; + vtkBox.intersectBox(boundsArray, o, r, placedP2, t); + publicAPI.setPoint2WorldPosition(placedP2); + + model.placed = 1; + model.validPick = 1; + publicAPI.buildRepresentation(); + }; + + publicAPI.computeInteractionState = (pos) => { + const p1State = model.point1Representation.computeInteractionState(pos); + const p2State = model.point2Representation.computeInteractionState(pos); + if (p1State === InteractionState.SELECTING) { + model.interactionState = State.ONP1; + publicAPI.setRepresentationState(State.ONP1); + } else if (p2State === InteractionState.SELECTING) { + model.interactionState = State.ONP2; + publicAPI.setRepresentationState(State.ONP2); + } else { + model.interactionState = State.OUTSIDE; + } + + if (model.interactionState !== State.OUTSIDE) { + return model.interactionState; + } + + let pos1 = publicAPI.getPoint1DisplayPosition(); + let pos2 = publicAPI.getPoint2DisplayPosition(); + const xyz = [pos[0], pos[1], 0.0]; + const p1 = [pos1[0], pos1[1], 0.0]; + const p2 = [pos2[0], pos2[1], 0.0]; + const tol = model.tolerance * model.tolerance; + + const out = vtkLine.distanceToLine(xyz, p1, p2); + const onLine = out.distance <= tol; + if (onLine && out.t < 1.0 && out.t > 0.0) { + model.interactionState = State.ONLINE; + publicAPI.setRepresentationState(State.ONLINE); + pos1 = publicAPI.getPoint1WorldPosition(); + pos2 = publicAPI.getPoint2WorldPosition(); + // TODO + // model.linePicker.pick(pos[0], pos[1], 0.0, model.renderer); + // const closest = model.linePicker.getPickPosition(); + // model.lineHandleRepresentation.setWorldPosition(closest); + } else { + model.interactionState = State.OUTSIDE; + publicAPI.setRepresentationState(State.OUTSIDE); + } + + return model.interactionState; + }; + + publicAPI.setRepresentationState = (state) => { + if (model.representationState === state) { + return; + } + model.representationState = state; + publicAPI.modified(); + + if (state === State.OUTSIDE) { + publicAPI.highlightPoint(0, 0); + publicAPI.highlightPoint(1, 0); + publicAPI.highlightLine(0); + } else if (state === State.ONP1) { + publicAPI.highlightPoint(0, 1); + publicAPI.highlightPoint(1, 0); + publicAPI.highlightLine(0); + } else if (state === State.ONP2) { + publicAPI.highlightPoint(0, 0); + publicAPI.highlightPoint(1, 1); + publicAPI.highlightLine(0); + } else if (state === State.ONLINE) { + publicAPI.highlightPoint(0, 0); + publicAPI.highlightPoint(1, 0); + publicAPI.highlightLine(1); + } else { + publicAPI.highlightPoint(0, 1); + publicAPI.highlightPoint(1, 1); + publicAPI.highlightLine(1); + } + }; + + publicAPI.sizeHandles = () => { + // Removed because radius is always close to 0 + // let radius = publicAPI.sizeHandlesInPixels(1.35, model.lineSource.getPoint1()); + // model.point1Representation.setHandleSize(radius); + // radius = publicAPI.sizeHandlesInPixels(1.35, model.lineSource.getPoint2()); + // model.point2Representation.setHandleSize(radius); + }; + + publicAPI.buildRepresentation = () => { + if (model.initializeDisplayPosition === 0 && model.renderer) { + publicAPI.setPoint1WorldPosition(model.lineSource.getPoint1()); + publicAPI.setPoint2WorldPosition(model.lineSource.getPoint2()); + model.validPick = 1; + model.initializeDisplayPosition = 1; + } + + model.point1Representation.setTolerance(model.tolerance); + model.point2Representation.setTolerance(model.tolerance); + // TODO + // model.lineHandleRepresentation.setTolerance(model.tolerance); + + const x1 = publicAPI.getPoint1WorldPosition(); + model.lineSource.setPoint1(...x1); + model.point1Representation.setWorldPosition(x1); + + const x2 = publicAPI.getPoint2WorldPosition(); + model.lineSource.setPoint2(...x2); + model.point2Representation.setWorldPosition(x2); + + publicAPI.sizeHandles(); + publicAPI.modified(); + }; + + publicAPI.highlightPoint = (pointId, highlight) => { + if (pointId === 0) { + if (highlight) { + model.point1Representation.setProperty(model.selectedEndPointProperty); + } else { + model.point1Representation.setProperty(model.endPointProperty); + } + } else if (pointId === 1) { + if (highlight) { + model.point2Representation.setProperty(model.selectedEndPoint2Property); + } else { + model.point2Representation.setProperty(model.endPoint2Property); + } + } else { + // TODO + // if (highlight) { + // model.lineHandleRepresentation.setProperty(model.selectedEndPoint2Property); + // } else { + // model.lineHandleRepresentation.setProperty(model.endPoint2Property); + // } + } + }; + + publicAPI.highlightLine = (highlight) => { + if (highlight) { + model.lineActor.setProperty(model.selectedLineProperty); + } else { + model.lineActor.setProperty(model.lineProperty); + } + }; + + publicAPI.setLineColor = (...color) => { + let col = []; + + if (Array.isArray(color[0])) { + col = color[0]; + } else { + for (let i = 0; i < color.length; i++) { + col.push(color[i]); + } + } + + if (col.length !== 3) { + return; + } + + if (model.lineActor.getProperty()) { + model.lineActor.getProperty().setColor(col[0], col[1], col[2]); + } + }; + + publicAPI.clampPosition = (x) => { + for (let i = 0; i < 3; i++) { + if (x[i] < model.initialBounds[2 * i]) { + x[i] = model.initialBounds[2 * i]; + } + if (x[i] > model.initialBounds[(2 * i) + 1]) { + x[i] = model.initialBounds[(2 * i) + 1]; + } + } + }; + + publicAPI.getBounds = () => { + publicAPI.buildRepresentation(); + model.boundingBox.setBounds(model.lineActor.getBounds()); + model.boundingBox.addBounds(model.point1Representation.getBounds()); + model.boundingBox.addBounds(model.point2Representation.getBounds()); + + return model.boundingBox.getBounds(); + }; + + publicAPI.getActors = () => { + const actors = []; + actors.push(model.point1Representation.getActors()); + actors.push(model.point2Representation.getActors()); + actors.push(model.lineActor); + return actors; + }; + + publicAPI.getNestedProps = () => publicAPI.getActors(); +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + point1Representation: null, + point2Representation: null, + lineSource: null, + lineMapper: null, + lineActor: null, + endPointProperty: null, + selectedEndPointProperty: null, + endPoint2Property: null, + selectedEndPoint2Property: null, + lineProperty: null, + selectedLineProperty: null, + tolerance: 5, + placed: 0, + representationState: State.OUTSIDE, + startP1: [0.0, 0.0, 0.0], + startP2: [0.0, 0.0, 0.0], + length: 0.0, + restrictFlag: Restrict.NONE, + initializeDisplayPosition: 0, + boundingBox: null, +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + // Inheritance + vtkWidgetRepresentation.extend(publicAPI, model, initialValues); + + // Getters/Setters + macro.get(publicAPI, model, [ + 'point1Representation', + 'point2Representation', + 'endPointProperty', + 'selectedEndPointProperty', + 'endPoint2Property', + 'selectedEndPoint2Property', + 'lineProperty', + 'selectedLineProperty', + ]); + + publicAPI.setHandleSize(5); + model.boundingBox = vtkBox.newInstance(); + model.point1Representation = vtkSphereHandleRepresentation.newInstance(); + model.point2Representation = vtkSphereHandleRepresentation.newInstance(); + const handleSize = 10; + model.point1Representation.setHandleSize(handleSize); + model.point2Representation.setHandleSize(handleSize); + // model.point1Representation.setSphereRadius(0.01); + // model.point2Representation.setSphereRadius(0.01); + + // Line + model.lineSource = vtkLineSource.newInstance({ + point1: [-0.5, 0, 0], + point2: [0.5, 0, 0], + resolution: 5, + }); + model.lineSource.setResolution(5); + model.lineMapper = vtkMapper.newInstance(); + model.lineMapper.setInputConnection(model.lineSource.getOutputPort()); + model.lineActor = vtkActor.newInstance(); + model.lineActor.setMapper(model.lineMapper); + + // Default properties + model.endPointProperty = vtkProperty.newInstance(); + model.endPointProperty.setColor(1, 1, 1); + model.selectedEndPointProperty = vtkProperty.newInstance(); + model.selectedEndPointProperty.setColor(0, 1, 0); + model.endPoint2Property = vtkProperty.newInstance(); + model.endPoint2Property.setColor(1, 1, 1); + model.selectedEndPoint2Property = vtkProperty.newInstance(); + model.selectedEndPoint2Property.setColor(0, 1, 0); + model.lineProperty = vtkProperty.newInstance(); + model.lineProperty.setAmbient(1.0); + model.lineProperty.setAmbientColor(1.0, 1.0, 1.0); + model.lineProperty.setLineWidth(2.0); + model.selectedLineProperty = vtkProperty.newInstance(); + model.selectedLineProperty.setAmbient(1.0); + model.selectedLineProperty.setColor(0.0, 1.0, 0.0); + model.selectedLineProperty.setLineWidth(2.0); + + // Pass the initial properties to the actor + model.point1Representation.setProperty(model.endPointProperty); + model.point2Representation.setProperty(model.endPoint2Property); + model.point1Representation.setWorldPosition(model.lineSource.getPoint1()); + model.point2Representation.setWorldPosition(model.lineSource.getPoint2()); + model.lineActor.setProperty(model.lineProperty); + + // Object methods + vtkLineRepresentation(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkLineRepresentation'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend }; diff --git a/Sources/Interaction/Widgets/LineWidget/Constants.js b/Sources/Interaction/Widgets/LineWidget/Constants.js new file mode 100644 index 00000000000..6acd2036689 --- /dev/null +++ b/Sources/Interaction/Widgets/LineWidget/Constants.js @@ -0,0 +1,10 @@ +export const WidgetState = { + START: 0, + DEFINE: 1, + MANIPULATE: 2, + ACTIVE: 3, +}; + +export default { + WidgetState, +}; diff --git a/Sources/Interaction/Widgets/LineWidget/api.md b/Sources/Interaction/Widgets/LineWidget/api.md new file mode 100644 index 00000000000..6e977e18218 --- /dev/null +++ b/Sources/Interaction/Widgets/LineWidget/api.md @@ -0,0 +1,18 @@ +## Introduction + +The line widget is a 3D widget for manipulating a line + +## See also + +[vtkAbstractWidget](./Interaction_Widgets_AbstractWidget.html) +[vtkHandleWidget](./Interaction_Widgets_HandleWidget.html) + +## setWidgetStateToStart() (DEFAULT) + +It defines that it's the user who places manually the two extremity of the line. +Then, the user can manipulate the line + +## setWidgetStateToManipulate() + +It defines that the line is already placed in the view. +Then, the use can manipulate the line \ No newline at end of file diff --git a/Sources/Interaction/Widgets/LineWidget/example/index.js b/Sources/Interaction/Widgets/LineWidget/example/index.js new file mode 100644 index 00000000000..c1023cc792f --- /dev/null +++ b/Sources/Interaction/Widgets/LineWidget/example/index.js @@ -0,0 +1,43 @@ +import 'vtk.js/Sources/favicon'; + +import vtkFullScreenRenderWindow from 'vtk.js/Sources/Rendering/Misc/FullScreenRenderWindow'; +import vtkLineWidget from 'vtk.js/Sources/Interaction/Widgets/LineWidget'; + +// ---------------------------------------------------------------------------- +// USER AVAILABLE INTERACTIONS +// ---------------------------------------------------------------------------- +// Sphere can be translated by clicking with mouse left on it +// Sphere can be scaled by clicking with mouse right + +// ---------------------------------------------------------------------------- +// Standard rendering code setup +// ---------------------------------------------------------------------------- + +const fullScreenRenderer = vtkFullScreenRenderWindow.newInstance(); +const renderer = fullScreenRenderer.getRenderer(); +const renderWindow = fullScreenRenderer.getRenderWindow(); +renderWindow.getInteractor().setInteractorStyle(null); + +// ---------------------------------------------------------------------------- +// Create widget +// ---------------------------------------------------------------------------- + +const widget = vtkLineWidget.newInstance(); +widget.setInteractor(renderWindow.getInteractor()); +widget.createDefaultRepresentation(); +widget.setEnable(1); +widget.setWidgetStateToStart(); +// widget.setWidgetStateToManipulate(); + +renderer.resetCamera(); +renderer.resetCameraClippingRange(); +renderWindow.render(); + +// ----------------------------------------------------------- +// Make some variables global so that you can inspect and +// modify objects in your browser's developer console: +// ----------------------------------------------------------- + +global.renderer = renderer; +global.renderWindow = renderWindow; +global.widget = widget; diff --git a/Sources/Interaction/Widgets/LineWidget/index.js b/Sources/Interaction/Widgets/LineWidget/index.js new file mode 100644 index 00000000000..df4ab1fc60a --- /dev/null +++ b/Sources/Interaction/Widgets/LineWidget/index.js @@ -0,0 +1,356 @@ +import HandleRepConstants from 'vtk.js/Sources/Interaction/Widgets/HandleRepresentation/Constants'; +import macro from 'vtk.js/Sources/macro'; +import vtkAbstractWidget from 'vtk.js/Sources/Interaction/Widgets/AbstractWidget'; +import vtkHandleWidget from 'vtk.js/Sources/Interaction/Widgets/HandleWidget'; +import vtkLineRepresentation from 'vtk.js/Sources/Interaction/Widgets/LineRepresentation'; +import { State } from 'vtk.js/Sources/Interaction/Widgets/LineRepresentation/Constants'; +import Constants from './Constants'; + +const { vtkErrorMacro } = macro; +const { WidgetState } = Constants; +const { InteractionState } = HandleRepConstants; + + +// ---------------------------------------------------------------------------- +// vtkHandleWidget methods +// ---------------------------------------------------------------------------- + +const events = [ + 'MouseMove', + 'LeftButtonPress', + 'LeftButtonRelease', + 'MiddleButtonPress', + 'MiddleButtonRelease', + 'RightButtonPress', + 'RightButtonRelease', +]; + +function vtkLineWidget(publicAPI, model) { + // Set our className + model.classHierarchy.push('vtkLineWidget'); + + const superClass = Object.assign({}, publicAPI); + + function setCursor(state) { + switch (state) { + case State.OUTSIDE: { + model.interactor.getView().setCursor('default'); + break; + } + default: { + model.interactor.getView().setCursor('pointer'); + } + } + } + + // Implemented method + publicAPI.listenEvents = () => { + if (!model.interactor) { + vtkErrorMacro('The interactor must be set before listening events'); + return; + } + events.forEach((eventName) => { + model.unsubscribes.push( + model.interactor[`on${eventName}`](() => { + if (publicAPI[`handle${eventName}`]) { + publicAPI[`handle${eventName}`](); + } + })); + }); + }; + + publicAPI.setInteractor = (i) => { + if (i === model.interactor) { + return; + } + + // if we already have an Interactor then stop observing it + if (model.interactor) { + while (model.unsubscribes.length) { + model.unsubscribes.pop().unsubscribe(); + } + } + + model.interactor = i; + + if (i) { + publicAPI.listenEvents(); + } + }; + + publicAPI.setEnable = (enabling) => { + const enable = model.enabled; + superClass.setEnable(enabling); + + if (model.interactor) { + model.point1Widget.setInteractor(model.interactor); + model.point2Widget.setInteractor(model.interactor); + } + + publicAPI.createDefaultRepresentation(); + model.point1Widget.setWidgetRep(model.widgetRep.getPoint1Representation()); + model.point2Widget.setWidgetRep(model.widgetRep.getPoint2Representation()); + + if (enabling) { + model.point1Widget.setEnable(1); + model.point2Widget.setEnable(1); + if (model.widgetState === WidgetState.START) { + model.widgetRep.setLineVisibility(0); + model.widgetRep.setPoint1Visibility(0); + model.widgetRep.setPoint2Visibility(0); + model.point1Widget.setEnable(0); + model.point2Widget.setEnable(0); + } else { + model.widgetRep.setLineVisibility(1); + model.widgetRep.setPoint1Visibility(1); + model.widgetRep.setPoint2Visibility(1); + + + // // Widgets can't be enabled together because of interaction event which doesn't + // // manage priority + // model.point1Widget.setEnable(1); + // model.point2Widget.setEnable(1); + } + } + + if (enabling && !enable) { + model.point1Widget.setWidgetRep(model.widgetRep.getPoint1Representation()); + model.point2Widget.setWidgetRep(model.widgetRep.getPoint2Representation()); + model.point1Widget.getWidgetRep().setRenderer(model.currentRenderer); + model.point2Widget.getWidgetRep().setRenderer(model.currentRenderer); + } else if (!enabling && enable) { + model.point1Widget.setEnable(0); + model.point2Widget.setEnable(0); + } + }; + + publicAPI.setWidgetStateToStart = () => { + model.widgetState = WidgetState.START; + model.currentHandle = 0; + model.widgetRep.buildRepresentation(); + publicAPI.setEnable(model.enabled); + }; + + publicAPI.setWidgetStateToManipulate = () => { + model.widgetState = WidgetState.MANIPULATE; + model.currentHandle = -1; + model.widgetRep.buildRepresentation(); + publicAPI.setEnable(model.enabled); + }; + + publicAPI.handleMouseMove = () => { + publicAPI.moveAction(); + }; + + publicAPI.handleLeftButtonPress = () => { + publicAPI.selectAction(); + }; + + publicAPI.handleLeftButtonRelease = () => { + publicAPI.endSelectAction(); + }; + + publicAPI.handleMiddleButtonPress = () => { + publicAPI.translateAction(); + }; + + publicAPI.handleMiddleButtonRelease = () => { + publicAPI.endSelectAction(); + }; + + publicAPI.handleRightButtonPress = () => { + publicAPI.scaleAction(); + }; + + publicAPI.handleRightButtonRelease = () => { + publicAPI.endSelectAction(); + }; + + publicAPI.selectAction = () => { + if (model.widgetRep.getInteractionState === InteractionState.OUTSIDE) { + return; + } + const pos = model.interactor.getEventPosition(model.interactor.getPointerIndex()); + const boundingContainer = model.interactor.getCanvas().getBoundingClientRect(); + const position = [pos.x - boundingContainer.left, pos.y + boundingContainer.top]; + + if (model.widgetState === WidgetState.START) { + const pos3D = model.point1Widget.getWidgetRep().displayToWorld(position, 0); + // The first time we click, the method is called twice + if (model.currentHandle <= 1) { + model.point1Widget.setEnable(0); + model.point2Widget.setEnable(0); + model.widgetRep.setPoint1Visibility(1); + model.widgetRep.setLineVisibility(1); + model.widgetRep.setPoint2Visibility(0); + model.widgetRep.setPoint1WorldPosition(pos3D); + // Trick to avoid a line with a zero length + // If the line has a zero length, it appears with bad extremities + pos3D[0] += 0.000000001; + model.widgetRep.setPoint2WorldPosition(pos3D); + model.currentHandle++; + } else { + model.point1Widget.setEnable(0); + model.widgetRep.setPoint1Visibility(1); + model.widgetRep.setLineVisibility(1); + model.widgetRep.setPoint2Visibility(1); + model.point2Widget.setEnable(0); + model.widgetRep.setPoint2WorldPosition(pos3D); + // When two points are placed, we go back to the native + publicAPI.setWidgetStateToManipulate(); + model.currentHandle++; + } + } else { + model.widgetState = WidgetState.ACTIVE; + model.currentHandle = 0; + publicAPI.invokeStartInteractionEvent(); + } + + model.widgetRep.startComplexWidgetInteraction(position); + + publicAPI.render(); + }; + + publicAPI.translateAction = () => { + if (model.widgetRep.getInteractionState === InteractionState.OUTSIDE) { + return; + } + const state = model.widgetRep.getInteractionState(); + if (state === State.ONP1) { + model.widgetRep.setInteractionState(State.TRANSLATINGP1); + } else if (state === State.ONP2) { + model.widgetRep.setInteractionState(State.TRANSLATINGP2); + } else { + model.widgetRep.setInteractionState(State.ONLINE); + } + + const pos = model.interactor.getEventPosition(model.interactor.getPointerIndex()); + const boundingContainer = model.interactor.getCanvas().getBoundingClientRect(); + const position = [pos.x - boundingContainer.left, pos.y + boundingContainer.top]; + + model.widgetState = WidgetState.ACTIVE; + model.widgetRep.startComplexWidgetInteraction(position); + publicAPI.invokeStartInteractionEvent(); + }; + + publicAPI.scaleAction = () => { + if (model.widgetRep.getInteractionState === InteractionState.OUTSIDE) { + return; + } + model.widgetRep.setInteractionState(State.SCALING); + const pos = model.interactor.getEventPosition(model.interactor.getPointerIndex()); + const boundingContainer = model.interactor.getCanvas().getBoundingClientRect(); + const position = [pos.x - boundingContainer.left, pos.y + boundingContainer.top]; + + model.widgetState = WidgetState.ACTIVE; + model.widgetRep.startComplexWidgetInteraction(position); + publicAPI.invokeStartInteractionEvent(); + }; + + publicAPI.moveAction = () => { + const pos = model.interactor.getEventPosition(model.interactor.getPointerIndex()); + const boundingContainer = model.interactor.getCanvas().getBoundingClientRect(); + const position = [pos.x - boundingContainer.left, pos.y + boundingContainer.top]; + + // Need to check where the mouse is + if (model.widgetState === WidgetState.MANIPULATE) { + model.interactor.disable(); // to avoid extra renders() + + model.point1Widget.setEnable(0); + model.point2Widget.setEnable(0); + if (model.currentHandle === 1) { + model.point1Widget.setEnable(1); + } + if (model.currentHandle === 2) { + model.widgetRep.complexWidgetInteraction(position); + } + + const state = model.widgetRep.computeInteractionState(position); + setCursor(state); + if (state !== State.OUTSIDE) { + if (state === State.ONP1) { + model.point1Widget.setEnable(1); + } else if (state === State.ONP2) { + model.point2Widget.setEnable(1); + } + } + model.interactor.enable(); + } else if (model.widgetState === WidgetState.START) { + model.widgetRep.setPoint1Visibility(1); + model.widgetRep.complexWidgetInteraction(position); + const pos3D = model.point1Widget.getWidgetRep().displayToWorld(position, 0); + if (model.currentHandle === 0) { + model.widgetRep.setPoint1WorldPosition(pos3D); + } + model.widgetRep.setPoint2WorldPosition(pos3D); + } else if (model.widgetState === WidgetState.ACTIVE) { + model.widgetRep.setPoint1WorldPosition(model.point1Widget.getWidgetRep().getWorldPosition()); + model.widgetRep.setPoint2WorldPosition(model.point2Widget.getWidgetRep().getWorldPosition()); + } + + publicAPI.invokeInteractionEvent(); + publicAPI.render(); + }; + + publicAPI.endSelectAction = () => { + if (model.widgetState === WidgetState.START) { + return; + } + const pos = model.interactor.getEventPosition(model.interactor.getPointerIndex()); + const boundingContainer = model.interactor.getCanvas().getBoundingClientRect(); + const position = [pos.x - boundingContainer.left, pos.y + boundingContainer.top]; + model.widgetRep.complexWidgetInteraction(position); + model.widgetRep.setPoint1WorldPosition(model.point1Widget.getWidgetRep().getWorldPosition()); + model.widgetRep.setPoint2WorldPosition(model.point2Widget.getWidgetRep().getWorldPosition()); + + model.widgetState = WidgetState.MANIPULATE; + publicAPI.invokeEndInteractionEvent(); + publicAPI.render(); + }; + + publicAPI.createDefaultRepresentation = () => { + if (!model.widgetRep) { + model.widgetRep = vtkLineRepresentation.newInstance(); + } + }; +} + +// ---------------------------------------------------------------------------- +// Object factory +// ---------------------------------------------------------------------------- + +const DEFAULT_VALUES = { + widgetState: WidgetState.START, + managesCursor: 1, + currentHandle: -1, + point1Widget: null, + point2Widget: null, +}; + +// ---------------------------------------------------------------------------- + +export function extend(publicAPI, model, initialValues = {}) { + Object.assign(model, DEFAULT_VALUES, initialValues); + + // Inheritance + vtkAbstractWidget.extend(publicAPI, model, initialValues); + + model.point1Widget = vtkHandleWidget.newInstance(); + model.point1Widget.setParent(publicAPI); + model.point1Widget.createDefaultRepresentation(); + + model.point2Widget = vtkHandleWidget.newInstance(); + model.point2Widget.setParent(publicAPI); + model.point2Widget.createDefaultRepresentation(); + + // Object methods + vtkLineWidget(publicAPI, model); +} + +// ---------------------------------------------------------------------------- + +export const newInstance = macro.newInstance(extend, 'vtkLineWidget'); + +// ---------------------------------------------------------------------------- + +export default { newInstance, extend }; diff --git a/Sources/Interaction/Widgets/SphereHandleRepresentation/index.js b/Sources/Interaction/Widgets/SphereHandleRepresentation/index.js index 314db4f36eb..4e8e21ac3cd 100644 --- a/Sources/Interaction/Widgets/SphereHandleRepresentation/index.js +++ b/Sources/Interaction/Widgets/SphereHandleRepresentation/index.js @@ -154,14 +154,16 @@ function vtkSphereHandleRepresentation(publicAPI, model) { } }; + publicAPI.displayToWorld = (eventPos, z) => vtkInteractorObserver.computeDisplayToWorld(model.renderer, + eventPos[0], eventPos[1], z); + publicAPI.complexWidgetInteraction = (eventPos) => { const focalPoint = vtkInteractorObserver.computeDisplayToWorld(model.renderer, model.lastPickPosition[0], model.lastPickPosition[1], model.lastPickPosition[2]); const z = focalPoint[2]; const prevPickPoint = vtkInteractorObserver.computeDisplayToWorld(model.renderer, model.lastEventPosition[0], model.lastEventPosition[1], z); - const pickPoint = vtkInteractorObserver.computeDisplayToWorld(model.renderer, - eventPos[0], eventPos[1], z); + const pickPoint = publicAPI.displayToWorld(eventPos, z); if (model.interactionState === InteractionState.SELECTING || model.interactionState === InteractionState.TRANSLATING) { diff --git a/Sources/Interaction/Widgets/index.js b/Sources/Interaction/Widgets/index.js index ef964fc0e1d..af9a5d4031b 100644 --- a/Sources/Interaction/Widgets/index.js +++ b/Sources/Interaction/Widgets/index.js @@ -1,6 +1,8 @@ import vtkAbstractWidget from './AbstractWidget'; import vtkHandleRepresentation from './HandleRepresentation'; import vtkHandleWidget from './HandleWidget'; +import vtkLineRepresentation from './LineRepresentation'; +import vtkLineWidget from './LineWidget'; import vtkPiecewiseGaussianWidget from './PiecewiseGaussianWidget'; import vtkPointPlacer from './PointPlacer'; import vtkSphereHandleRepresentation from './SphereHandleRepresentation'; @@ -10,6 +12,8 @@ export default { vtkAbstractWidget, vtkHandleRepresentation, vtkHandleWidget, + vtkLineRepresentation, + vtkLineWidget, vtkPiecewiseGaussianWidget, vtkPointPlacer, vtkSphereHandleRepresentation,