diff --git a/src/components/XircuitsApp.ts b/src/components/XircuitsApp.ts index 07abc948..2461f960 100644 --- a/src/components/XircuitsApp.ts +++ b/src/components/XircuitsApp.ts @@ -1,7 +1,6 @@ import * as SRD from '@projectstorm/react-diagrams'; import { CustomNodeFactory } from "./node/CustomNodeFactory"; import { CustomNodeModel } from './node/CustomNodeModel'; -import { ZoomCanvasAction } from '@projectstorm/react-canvas-core'; import { CustomActionEvent } from '../commands/CustomActionEvent'; import { ILabShell, JupyterFrontEnd } from '@jupyterlab/application'; import { CustomDiagramState } from './state/CustomDiagramState' @@ -10,6 +9,7 @@ import { ParameterLinkFactory, TriangleLinkFactory } from './link/CustomLinkFact import { PointModel } from '@projectstorm/react-diagrams'; import { Point } from '@projectstorm/geometry'; import { BaseComponentLibrary } from '../tray_library/BaseComponentLib'; +import { CustomPanAndZoomCanvasAction } from "./actions/CustomPanAndZoomCanvasAction"; export class XircuitsApplication { @@ -24,7 +24,7 @@ export class XircuitsApplication { this.diagramEngine.getNodeFactories().registerFactory(new CustomNodeFactory(app, shell)); this.diagramEngine.getLinkFactories().registerFactory(new ParameterLinkFactory()); this.diagramEngine.getLinkFactories().registerFactory(new TriangleLinkFactory()); - this.diagramEngine.getActionEventBus().registerAction(new ZoomCanvasAction({ inverseZoom: true })) + this.diagramEngine.getActionEventBus().registerAction(new CustomPanAndZoomCanvasAction()) this.diagramEngine.getActionEventBus().registerAction(new CustomActionEvent({ app })); this.diagramEngine.getStateMachine().pushState(new CustomDiagramState()); diff --git a/src/components/XircuitsBodyWidget.tsx b/src/components/XircuitsBodyWidget.tsx index a09ccb2f..3f4c2dd5 100644 --- a/src/components/XircuitsBodyWidget.tsx +++ b/src/components/XircuitsBodyWidget.tsx @@ -1098,6 +1098,14 @@ export const BodyWidget: FC = ({ hidePanel(); }; + useEffect(() => { + const canvas = xircuitsApp.getDiagramEngine().getCanvas() + canvas.addEventListener('wheel', preventDefault); + return () => { + canvas.removeEventListener('wheel', preventDefault); + } + }, [xircuitsApp.getDiagramEngine().getCanvas()]) + return ( diff --git a/src/components/actions/CustomPanAndZoomCanvasAction.ts b/src/components/actions/CustomPanAndZoomCanvasAction.ts new file mode 100644 index 00000000..a11eb0e1 --- /dev/null +++ b/src/components/actions/CustomPanAndZoomCanvasAction.ts @@ -0,0 +1,74 @@ +import { WheelEvent } from 'react'; +import { Action, ActionEvent, InputType } from '@projectstorm/react-canvas-core'; + +export interface CustomPanAndZoomCanvasActionOptions { + inverseZoom?: boolean; +} + +export class CustomPanAndZoomCanvasAction extends Action { + constructor(options: CustomPanAndZoomCanvasActionOptions = {}) { + super({ + type: InputType.MOUSE_WHEEL, + fire: (actionEvent: ActionEvent) => { + const { event } = actionEvent; + // we can block layer rendering because we are only targeting the transforms + for (let layer of this.engine.getModel().getLayers()) { + layer.allowRepaint(false); + } + + const model = this.engine.getModel(); + event.stopPropagation(); + if (event.ctrlKey) { + // Pinch and zoom gesture + const oldZoomFactor = this.engine.getModel().getZoomLevel() / 100; + + let scrollDelta = options.inverseZoom ? event.deltaY : -event.deltaY; + //check if it is pinch gesture + if (event.ctrlKey && scrollDelta % 1 !== 0) { + /* + Chrome and Firefox sends wheel event with deltaY that + have fractional part, also `ctrlKey` prop of the event is true + though ctrl isn't pressed + */ + scrollDelta /= 3; + } else { + scrollDelta /= 60; + } + + if (model.getZoomLevel() + scrollDelta > 10) { + model.setZoomLevel(model.getZoomLevel() + scrollDelta); + } + + const zoomFactor = model.getZoomLevel() / 100; + + const boundingRect = event.currentTarget.getBoundingClientRect(); + const clientWidth = boundingRect.width; + const clientHeight = boundingRect.height; + // compute difference between rect before and after scroll + const widthDiff = clientWidth * zoomFactor - clientWidth * oldZoomFactor; + const heightDiff = clientHeight * zoomFactor - clientHeight * oldZoomFactor; + // compute mouse coords relative to canvas + const clientX = event.clientX - boundingRect.left; + const clientY = event.clientY - boundingRect.top; + + // compute width and height increment factor + const xFactor = (clientX - model.getOffsetX()) / oldZoomFactor / clientWidth; + const yFactor = (clientY - model.getOffsetY()) / oldZoomFactor / clientHeight; + + model.setOffset(model.getOffsetX() - widthDiff * xFactor, model.getOffsetY() - heightDiff * yFactor); + } else { + // Pan gesture + let yDelta = options.inverseZoom ? -event.deltaY : event.deltaY; + let xDelta = options.inverseZoom ? -event.deltaX : event.deltaX; + model.setOffset(model.getOffsetX() - xDelta, model.getOffsetY() - yDelta); + } + this.engine.repaintCanvas(); + + // re-enable rendering + for (let layer of this.engine.getModel().getLayers()) { + layer.allowRepaint(true); + } + } + }); + } +} \ No newline at end of file diff --git a/src/components/link/CustomLinkFactory.tsx b/src/components/link/CustomLinkFactory.tsx index b92d1cf2..8728faa5 100644 --- a/src/components/link/CustomLinkFactory.tsx +++ b/src/components/link/CustomLinkFactory.tsx @@ -1,4 +1,5 @@ -import { DefaultLinkFactory } from '@projectstorm/react-diagrams'; +import { DefaultLinkFactory, DefaultLinkWidget } from "@projectstorm/react-diagrams"; +import { LinkWidget } from '@projectstorm/react-diagrams-core'; import * as React from 'react'; import { ParameterLinkModel, TriangleLinkModel } from './CustomLinkModel'; import styled from '@emotion/styled'; @@ -46,7 +47,96 @@ function removeHover(model: TriangleLinkModel | ParameterLinkModel){ } } -export class ParameterLinkFactory extends DefaultLinkFactory { +class SelectOnClickLinkWidget extends DefaultLinkWidget { + constructor(type) { + super(type); + } + addPointToLink(event: React.MouseEvent, index: number) { + if ( + event.ctrlKey && + !this.props.link.isLocked() && + this.props.link.getPoints().length - 1 <= this.props.diagramEngine.getMaxNumberPointsPerLink() + ) { + event.stopPropagation(); + + const position = this.props.diagramEngine.getRelativeMousePoint(event); + const point = this.props.link.point(position.x, position.y, index); + event.persist(); + this.forceUpdate(() => { + this.props.diagramEngine.getActionEventBus().fireAction({ + event, + model: point + }); + }); + } + } + + render() { + //ensure id is present for all points on the path + var points = this.props.link.getPoints(); + var paths = []; + this.refPaths = []; + + if (points.length === 2) { + paths.push( + this.generateLink( + this.props.link.getSVGPath(), + { + onMouseDown: (event) => { + this.props.selected?.(event); + this.addPointToLink(event, 1); + } + }, + '0' + ) + ); + + // draw the link as dangeling + if (this.props.link.getTargetPort() == null) { + paths.push(this.generatePoint(points[1])); + } + } else { + //draw the multiple anchors and complex line instead + for (let j = 0; j < points.length - 1; j++) { + paths.push( + this.generateLink( + LinkWidget.generateLinePath(points[j], points[j + 1]), + { + 'data-linkid': this.props.link.getID(), + 'data-point': j, + onMouseDown: (event) => { + this.props.selected?.(event); + this.addPointToLink(event, j + 1); + } + }, + j + ) + ); + } + + if (this.renderPoints()) { + //render the circles + for (let i = 1; i < points.length - 1; i++) { + paths.push(this.generatePoint(points[i])); + } + + if (this.props.link.getTargetPort() == null) { + paths.push(this.generatePoint(points[points.length - 1])); + } + } + } + + return {paths}; + } +} + +class SelectOnClickLinkFactory extends DefaultLinkFactory { + generateReactWidget(event: any): JSX.Element { + return ; + } +} + +export class ParameterLinkFactory extends SelectOnClickLinkFactory { constructor() { super('parameter-link'); } @@ -69,7 +159,7 @@ export class ParameterLinkFactory extends DefaultLinkFactory { } } -export class TriangleLinkFactory extends DefaultLinkFactory { +export class TriangleLinkFactory extends SelectOnClickLinkFactory { constructor() { super('triangle-link'); }