import React from "react"; import { observable, action, runInAction, makeObservable } from "mobx"; import { Point, Rect, rectEqual, rectClone } from "eez-studio-shared/geometry"; import { CONF_ACTIVATE_SNAP_TO_LINES_AFTER_TIME, findClosestHorizontalSnapLinesToPosition, findClosestVerticalSnapLinesToPosition, SnapLines } from "project-editor/flow/editor/snap-lines"; import { addAlphaToColor } from "eez-studio-shared/color"; import { theme } from "eez-studio-ui/theme"; import type { IFlowContext } from "project-editor/flow/flow-interfaces"; import { getSelectedObjectsBoundingRect, getObjectIdFromPoint, getObjectBoundingRect } from "project-editor/flow/editor/bounding-rects"; import type { TreeObjectAdapter } from "project-editor/core/objectAdapter"; import type { Transform } from "project-editor/flow/editor/transform"; import { generateConnectionLinePath } from "project-editor/flow/connection-line/connection-line-shape"; import type { Flow } from "project-editor/flow/flow"; import type { ConnectionLine } from "project-editor/flow/connection-line"; import { getId } from "project-editor/core/object"; import type { Component } from "project-editor/flow/component"; import { ProjectEditor } from "project-editor/project-editor-interface"; import type { LVGLWidget } from "project-editor/lvgl/widgets/Base"; //////////////////////////////////////////////////////////////////////////////// const CONNECTION_LINE_DRAW_THEME = { lineColor: "rgba(128, 128, 128, 1)", lineWidth: 2.0, connectedLineColor: "rgb(32, 192, 32)", connectedLineWidth: 2.0 }; //////////////////////////////////////////////////////////////////////////////// export interface IMouseHandler { cursor: string; lastPointerEvent: IPointerEvent; down(context: IFlowContext, event: IPointerEvent): void; move(context: IFlowContext, event: IPointerEvent): void; up(context: IFlowContext, cancel: boolean): void; render?(context: IFlowContext): React.ReactNode; onTransformChanged(context: IFlowContext): void; } export interface IPointerEvent { clientX: number; clientY: number; movementX: number; movementY: number; ctrlKey: boolean; shiftKey: boolean; timeStamp: number; } //////////////////////////////////////////////////////////////////////////////// export function isSelectionMoveable(context: IFlowContext) { return context.viewState.selectedObjects.find(object => object.isMoveable); } //////////////////////////////////////////////////////////////////////////////// export class MouseHandler implements IMouseHandler { constructor() {} timeAtDown: number; elapsedTime: number; offsetPointAtDown: Point; lastOffsetPoint: Point; offsetDistance: Point; movement: Point; distance: number; modelPointAtDown: Point; lastModelPoint: Point; modelDistance: Point; cursor: string = "default"; transform: Transform; lastPointerEvent!: IPointerEvent; down(context: IFlowContext, event: IPointerEvent) { this.transform = context.viewState.transform; this.timeAtDown = event.timeStamp; this.lastOffsetPoint = this.offsetPointAtDown = this.transform.pointerEventToOffsetPoint(event); this.offsetDistance = { x: 0, y: 0 }; this.distance = 0; this.movement = { x: 0, y: 0 }; this.modelPointAtDown = this.transform.pointerEventToPagePoint(event); this.modelDistance = { x: 0, y: 0 }; } move(context: IFlowContext, event: IPointerEvent) { this.transform = context.viewState.transform; this.elapsedTime = event.timeStamp - this.timeAtDown; let offsetPoint = this.transform.pointerEventToOffsetPoint(event); this.offsetDistance = { x: offsetPoint.x - this.offsetPointAtDown.x, y: offsetPoint.y - this.offsetPointAtDown.y }; this.distance = Math.sqrt( this.offsetDistance.x * this.offsetDistance.x + this.offsetDistance.y * this.offsetDistance.y ); this.movement = { x: offsetPoint.x - this.lastOffsetPoint.x, y: offsetPoint.y - this.lastOffsetPoint.y }; this.lastOffsetPoint = offsetPoint; this.lastModelPoint = this.transform.pointerEventToPagePoint(event); this.modelDistance = { x: this.lastModelPoint.x - this.modelPointAtDown.x, y: this.lastModelPoint.y - this.modelPointAtDown.y }; } up(context: IFlowContext, cancel: boolean) {} onTransformChanged(context: IFlowContext) { const transform = context.viewState.transform; let point = this.transform.offsetToPagePoint(this.offsetPointAtDown); this.offsetPointAtDown = transform.pageToOffsetPoint(point); point = this.transform.offsetToPagePoint(this.lastOffsetPoint); this.lastOffsetPoint = transform.pageToOffsetPoint(point); this.offsetDistance = { x: this.lastOffsetPoint.x - this.offsetPointAtDown.x, y: this.lastOffsetPoint.y - this.offsetPointAtDown.y }; this.distance = Math.sqrt( this.offsetDistance.x * this.offsetDistance.x + this.offsetDistance.y * this.offsetDistance.y ); this.modelPointAtDown = transform.offsetToPagePoint( this.offsetPointAtDown ); this.lastModelPoint = transform.offsetToPagePoint(this.lastOffsetPoint); this.movement = { x: 0, y: 0 }; const pointerEvent = Object.assign({}, this.lastPointerEvent); point = this.transform.clientToPagePoint({ x: pointerEvent.clientX, y: pointerEvent.clientY }); const clientPoint = transform.pageToClientPoint(point); pointerEvent.movementX = clientPoint.x - pointerEvent.clientX; pointerEvent.movementY = clientPoint.y - pointerEvent.clientY; pointerEvent.clientX = clientPoint.x; pointerEvent.clientY = clientPoint.y; this.transform = transform; this.move(context, pointerEvent); } } //////////////////////////////////////////////////////////////////////////////// export class PanMouseHandler extends MouseHandler { totalMovement = { x: 0, y: 0 }; down(context: IFlowContext, event: IPointerEvent) { super.down(context, event); } move(context: IFlowContext, event: IPointerEvent) { super.move(context, event); context.viewState.transform.translateBy(this.movement); this.totalMovement.x += this.movement.x; this.totalMovement.y += this.movement.y; } } //////////////////////////////////////////////////////////////////////////////// export class LVGLPanMouseHandler extends MouseHandler { constructor(public lvglWidget: LVGLWidget) { super(); } move(context: IFlowContext, event: IPointerEvent) { super.move(context, event); runInAction(() => { this.lvglWidget._xScroll = this.lvglWidget._xScroll2 = this.lvglWidget._xScroll2 - this.movement.x; this.lvglWidget._yScroll = this.lvglWidget._yScroll2 = this.lvglWidget._yScroll2 - this.movement.y; }); } } //////////////////////////////////////////////////////////////////////////////// export class RubberBandSelectionMouseHandler extends MouseHandler { rubberBendRect: Rect | undefined; constructor() { super(); makeObservable(this, { rubberBendRect: observable, move: action }); } down(context: IFlowContext, event: IPointerEvent) { super.down(context, event); context.viewState.deselectAllObjects(); } move(context: IFlowContext, event: IPointerEvent) { super.move(context, event); let left; let top; let width; let height; if (this.offsetPointAtDown.x < this.lastOffsetPoint.x) { left = this.offsetPointAtDown.x; width = this.lastOffsetPoint.x - this.offsetPointAtDown.x; } else { left = this.lastOffsetPoint.x; width = this.offsetPointAtDown.x - this.lastOffsetPoint.x; } if (this.offsetPointAtDown.y < this.lastOffsetPoint.y) { top = this.offsetPointAtDown.y; height = this.lastOffsetPoint.y - this.offsetPointAtDown.y; } else { top = this.lastOffsetPoint.y; height = this.offsetPointAtDown.y - this.lastOffsetPoint.y; } const rubberBendRect = { left, top, width, height }; this.rubberBendRect = rubberBendRect; context.viewState.selectObjects( context.document.getObjectsInsideRect( context.viewState.transform.offsetToPageRect(rubberBendRect) ) ); } up(context: IFlowContext, cancel: boolean) { super.up(context, cancel); runInAction(() => { this.rubberBendRect = undefined; }); } render(context: IFlowContext) { return ( this.rubberBendRect && (
) ); } } //////////////////////////////////////////////////////////////////////////////// export class MouseHandlerWithSnapLines extends MouseHandler { snapLines = new SnapLines(); down(context: IFlowContext, event: IPointerEvent) { super.down(context, event); this.snapLines.find(context); } move(context: IFlowContext, event: IPointerEvent) { super.move(context, event); this.snapLines.enabled = !event.shiftKey && this.elapsedTime > CONF_ACTIVATE_SNAP_TO_LINES_AFTER_TIME; } render(context: IFlowContext) { const rect = getSelectedObjectsBoundingRect(context.viewState); const dxMouseDrag = context.viewState.dxMouseDrag; const dyMouseDrag = context.viewState.dyMouseDrag; if (!context.projectStore.projectTypeTraits.isDashboard) { rect.left += dxMouseDrag ?? 0; rect.top += dyMouseDrag ?? 0; } return this.snapLines.render(context, rect); } } //////////////////////////////////////////////////////////////////////////////// export class DragMouseHandler extends MouseHandlerWithSnapLines { selectedObjects: TreeObjectAdapter[]; changed: boolean = false; rects: Rect[] = []; selectionBoundingRectAtDown: Rect; objectPositionsAtDown: Point[]; left: number; top: number; selectionNode: HTMLElement; objectNodes: HTMLElement[]; constructor() { super(); makeObservable(this, { move: action, up: action }); } getConnectionLineAlignedY( context: IFlowContext, event: IPointerEvent, left: number, top: number ) { if (!event.ctrlKey) { return undefined; } const dxMouseDrag = left - this.selectionBoundingRectAtDown.left; const dyMouseDrag = top - this.selectionBoundingRectAtDown.top; let foundConnectionLine; let foundLength = 0; for (const connectionLine of (context.document.flow.object as Flow) .connectionLines) { const sourceComponentIndex = this.selectedObjects.findIndex( selectedObject => selectedObject.object == connectionLine.sourceComponent ); const targetComponentIndex = this.selectedObjects.findIndex( selectedObject => selectedObject.object == connectionLine.targetComponent ); if ( (sourceComponentIndex != -1 && targetComponentIndex != -1) || (sourceComponentIndex == -1 && targetComponentIndex == -1) ) { continue; } let sourceOutputX; let sourceOutputY; let targetInputX; let targetInputY; const outputPos = connectionLine.sourceComponent.geometry.outputs[ connectionLine.output ].position; const inputPos = connectionLine.targetComponent.geometry.inputs[ connectionLine.input ].position; if (sourceComponentIndex != -1) { sourceOutputX = this.objectPositionsAtDown[sourceComponentIndex].x + dxMouseDrag + outputPos.x; sourceOutputY = this.objectPositionsAtDown[sourceComponentIndex].y + dyMouseDrag + outputPos.y; targetInputX = connectionLine.targetComponent.geometry.left + inputPos.x; targetInputY = connectionLine.targetComponent.geometry.top + inputPos.y; } else { sourceOutputX = connectionLine.sourceComponent.geometry.left + outputPos.x; sourceOutputY = connectionLine.sourceComponent.geometry.top + outputPos.y; targetInputX = this.objectPositionsAtDown[targetComponentIndex].x + dxMouseDrag + inputPos.x; targetInputY = this.objectPositionsAtDown[targetComponentIndex].y + dyMouseDrag + inputPos.y; } if (sourceOutputX < targetInputX) { const dx = targetInputX - sourceOutputX; const dy = targetInputY - sourceOutputY; const length = dx * dx + dy * dy; if (!foundConnectionLine || length < foundLength) { foundConnectionLine = connectionLine; foundLength = length; } } } if (!foundConnectionLine) { return undefined; } const sourceComponentIndex = this.selectedObjects.findIndex( selectedObject => selectedObject.object == foundConnectionLine.sourceComponent ); if (sourceComponentIndex != -1) { const topTarget = foundConnectionLine.targetComponent.top; const topBounding = this.selectionBoundingRectAtDown.top; const topSource = this.objectPositionsAtDown[sourceComponentIndex].y; const sourceOutputY = foundConnectionLine.sourceComponent.geometry.outputs[ foundConnectionLine.output ].position.y; const targetInputY = foundConnectionLine.targetComponent.geometry.inputs[ foundConnectionLine.input ].position.y; return ( topTarget + (topBounding - topSource) - (sourceOutputY - targetInputY) ); } else { const targetComponentIndex = this.selectedObjects.findIndex( selectedObject => selectedObject.object == foundConnectionLine.targetComponent ); const topSource = foundConnectionLine.sourceComponent.top; const topBounding = this.selectionBoundingRectAtDown.top; const topTarget = this.objectPositionsAtDown[targetComponentIndex].y; const sourceOutputY = foundConnectionLine.sourceComponent.geometry.outputs[ foundConnectionLine.output ].position.y; const targetInputY = foundConnectionLine.targetComponent.geometry.inputs[ foundConnectionLine.input ].position.y; return ( topSource + (topBounding - topTarget) - (targetInputY - sourceOutputY) ); } } down(context: IFlowContext, event: IPointerEvent) { super.down(context, event); context.document.onDragStart(); this.selectionBoundingRectAtDown = rectClone( getSelectedObjectsBoundingRect(context.viewState) ); this.selectedObjects = context.viewState.selectedObjects.filter( selectedObject => selectedObject.isMoveable ); this.objectPositionsAtDown = this.selectedObjects.map(object => ({ x: object.rect.left, y: object.rect.top })); this.left = this.selectionBoundingRectAtDown.left; this.top = this.selectionBoundingRectAtDown.top; } move(context: IFlowContext, event: IPointerEvent) { super.move(context, event); this.left += this.movement.x / context.viewState.transform.scale; this.top += this.movement.y / context.viewState.transform.scale; if (this.elapsedTime < 200 && this.distance < 10) { return; } let { left, top } = this.snapLines.dragSnap( this.left, this.top, this.selectionBoundingRectAtDown.width, this.selectionBoundingRectAtDown.height ); const connectionLineAlignedY = this.getConnectionLineAlignedY( context, event, left, top ); if (connectionLineAlignedY) { top = connectionLineAlignedY; } const viewState = context.viewState; viewState.dxMouseDrag = left - this.selectionBoundingRectAtDown.left; viewState.dyMouseDrag = top - this.selectionBoundingRectAtDown.top; if (!this.selectionNode) { const container = document.getElementById( context.viewState.containerId )!; this.selectionNode = container.querySelector( ".EezStudio_FlowEditorSelection_Draggable" ) as HTMLElement; this.objectNodes = this.selectedObjects.map( selectedObject => container.querySelector( `[data-eez-flow-object-id="${selectedObject.id}"]` ) as HTMLElement ); } if (!context.projectStore.projectTypeTraits.isDashboard) { this.selectionNode.style.transform = `translate(${Math.round( viewState.dxMouseDrag * context.viewState.transform.scale )}px, ${Math.round( viewState.dyMouseDrag * context.viewState.transform.scale )}px)`; } this.changed = false; for (let i = 0; i < this.selectedObjects.length; ++i) { const object = this.selectedObjects[i]; this.rects[i] = { left: Math.round( this.objectPositionsAtDown[i].x + viewState.dxMouseDrag ), top: Math.round( this.objectPositionsAtDown[i].y + viewState.dyMouseDrag ), width: object.rect.width, height: object.rect.height }; if (!rectEqual(this.rects[i], object.rect)) { this.changed = true; } const node = this.objectNodes[i]; node.style.left = this.rects[i].left + "px"; node.style.top = this.rects[i].top + "px"; } } up(context: IFlowContext, cancel: boolean) { super.up(context, cancel); if (this.objectNodes) { for (let i = 0; i < this.objectNodes.length; ++i) { const node = this.objectNodes[i]; node.style.left = this.objectPositionsAtDown[i].x + "px"; node.style.top = this.objectPositionsAtDown[i].y + "px"; } } if (this.changed) { for (let i = 0; i < this.selectedObjects.length; ++i) { const object = this.selectedObjects[i]; const rect = this.rects[i]; if (!rectEqual(rect, object.rect)) { object.rect = rect; } } } if (!context.projectStore.projectTypeTraits.isDashboard) { if (this.selectionNode) { this.selectionNode.style.transform = ""; this.selectionNode.style.display = "none"; } } context.viewState.dxMouseDrag = undefined; context.viewState.dyMouseDrag = undefined; context.document.onDragEnd(); } } //////////////////////////////////////////////////////////////////////////////// type ResizeHandleType = | "nw-resize" | "n-resize" | "ne-resize" | "w-resize" | "e-resize" | "sw-resize" | "s-resize" | "se-resize"; export class ResizeMouseHandler extends MouseHandlerWithSnapLines { savedRect: Rect; rect: Rect; changed: boolean = false; object: TreeObjectAdapter; xSnapOffset: number = 0; ySnapOffset: number = 0; constructor(private handleType: ResizeHandleType) { super(); makeObservable(this, { move: action }); } down(context: IFlowContext, event: IPointerEvent) { super.down(context, event); this.object = context.viewState.selectedObjects[0]; context.document.onDragStart(); this.savedRect = rectClone(this.object.rect); this.rect = rectClone(this.object.rect); const rect = getObjectBoundingRect(context.viewState, this.object); this.xSnapOffset = rect.left - this.rect.left; this.ySnapOffset = rect.top - this.rect.top; } snapX(x: number) { x = Math.round(x); if (this.snapLines.enabled) { let findResult = findClosestVerticalSnapLinesToPosition( this.snapLines.lines, x + this.xSnapOffset ); return findResult ? Math.round(findResult.lines[0].pos) - this.xSnapOffset : x; } else { return x; } } snapY(y: number) { y = Math.round(y); if (this.snapLines.enabled) { let findResult = findClosestHorizontalSnapLinesToPosition( this.snapLines.lines, y + this.ySnapOffset ); return findResult ? Math.round(findResult.lines[0].pos) - this.ySnapOffset : y; } else { return y; } } moveTop(context: IFlowContext, savedRect: Rect, rect: Rect) { let bottom = rect.top + rect.height; rect.top = this.snapY( savedRect.top + this.offsetDistance.y / context.viewState.transform.scale ); if (rect.top >= bottom) { rect.top = bottom - 1; } rect.height = bottom - rect.top; } moveLeft(context: IFlowContext, savedRect: Rect, rect: Rect) { let right = rect.left + rect.width; rect.left = this.snapX( savedRect.left + this.offsetDistance.x / context.viewState.transform.scale ); if (rect.left >= right) { rect.left = right - 1; } rect.width = right - rect.left; } moveBottom(context: IFlowContext, savedRect: Rect, rect: Rect) { let bottom = this.snapY( savedRect.top + savedRect.height + this.offsetDistance.y / context.viewState.transform.scale ); if (bottom <= rect.top) { bottom = rect.top + 1; } rect.height = bottom - rect.top; } moveRight(context: IFlowContext, savedRect: Rect, rect: Rect) { let right = this.snapX( savedRect.left + savedRect.width + this.offsetDistance.x / context.viewState.transform.scale ); if (right <= rect.left) { right = rect.left + 1; } rect.width = right - rect.left; } resizeRect(context: IFlowContext, savedRect: Rect, rect: Rect) { if (this.handleType === "nw-resize") { this.moveTop(context, savedRect, rect); this.moveLeft(context, savedRect, rect); } else if (this.handleType === "n-resize") { this.moveTop(context, savedRect, rect); } else if (this.handleType === "ne-resize") { this.moveTop(context, savedRect, rect); this.moveRight(context, savedRect, rect); } else if (this.handleType === "w-resize") { this.moveLeft(context, savedRect, rect); } else if (this.handleType === "e-resize") { this.moveRight(context, savedRect, rect); } else if (this.handleType === "sw-resize") { this.moveBottom(context, savedRect, rect); this.moveLeft(context, savedRect, rect); } else if (this.handleType === "s-resize") { this.moveBottom(context, savedRect, rect); } else if (this.handleType === "se-resize") { this.moveBottom(context, savedRect, rect); this.moveRight(context, savedRect, rect); } } move(context: IFlowContext, event: IPointerEvent) { super.move(context, event); this.resizeRect(context, this.savedRect, this.rect); if (!rectEqual(this.rect, this.object.rect)) { this.changed = true; this.object.rect = { left: Math.floor(this.rect.left), top: Math.floor(this.rect.top), width: Math.floor(this.rect.width), height: Math.floor(this.rect.height) }; } } up(context: IFlowContext, cancel: boolean) { super.up(context, cancel); context.document.onDragEnd(); } } //////////////////////////////////////////////////////////////////////////////// const lineStyle = { stroke: CONNECTION_LINE_DRAW_THEME.lineColor, strokeWidth: CONNECTION_LINE_DRAW_THEME.lineWidth, fill: "none" }; const connectedLineStyle = { stroke: CONNECTION_LINE_DRAW_THEME.connectedLineColor, strokeWidth: CONNECTION_LINE_DRAW_THEME.connectedLineWidth, fill: "none" }; export class NewConnectionLineFromOutputMouseHandler extends MouseHandler { startPoint: Point; endPoint: Point; target: | { objectId: string; connectionInput: string; } | undefined; cursor: string = "crosshair"; sourceRect: Rect; targetRect: Rect | undefined; constructor( private sourceObject: TreeObjectAdapter, private connectionOutput: string ) { super(); makeObservable(this, { startPoint: observable, endPoint: observable, target: observable.shallow, down: action, move: action, onTransformChanged: action }); } down(context: IFlowContext, event: IPointerEvent) { super.down(context, event); context.document.onDragStart(); const container = document.getElementById( context.viewState.containerId )!; const sourceNode = container!.querySelector( `[data-eez-flow-object-id="${this.sourceObject.id}"]` )!; const sourceNodeBoundingClientRect = sourceNode.getBoundingClientRect(); this.sourceRect = context.viewState.transform.clientToOffsetRect( sourceNodeBoundingClientRect ); const nodeOutput = getConnectionOutputNode( sourceNode, this.connectionOutput )!; const nodeOutputBoundingClientRect = nodeOutput.getBoundingClientRect(); const nodeOutputPageRect = context.viewState.transform.clientToOffsetRect( nodeOutputBoundingClientRect ); this.startPoint = { x: nodeOutputPageRect.left + nodeOutputPageRect.width, y: nodeOutputPageRect.top + nodeOutputPageRect.height / 2 }; this.endPoint = this.lastOffsetPoint; this.target = undefined; this.targetRect = undefined; } move(context: IFlowContext, event: IPointerEvent) { super.move(context, event); const result = getObjectIdFromPoint( context.document, context.viewState, this.lastModelPoint ); if ( result && result.connectionInput && !context.document.connectionExists( this.sourceObject.id, this.connectionOutput, result.id, result.connectionInput ) ) { const container = document.getElementById( context.viewState.containerId )!; const targetNode = container!.querySelector( `[data-eez-flow-object-id="${result.id}"]` )!; const targetNodeBoundingClientRect = targetNode.getBoundingClientRect(); this.targetRect = context.viewState.transform.clientToOffsetRect( targetNodeBoundingClientRect ); const nodeInput = getConnectionInputNode( targetNode, result.connectionInput )!; const nodeInputBoundingClientRect = nodeInput.getBoundingClientRect(); const nodeInputPageRect = context.viewState.transform.clientToOffsetRect( nodeInputBoundingClientRect ); this.endPoint = { x: nodeInputPageRect.left, y: nodeInputPageRect.top + nodeInputPageRect.height / 2 }; this.target = { objectId: result.id, connectionInput: result.connectionInput }; } else { this.endPoint = this.lastOffsetPoint; this.target = undefined; this.targetRect = undefined; } } up(context: IFlowContext, cancel: boolean) { super.up(context, cancel); if (!cancel) { if (this.target) { context.document.connect( this.sourceObject.id, this.connectionOutput, this.target.objectId, this.target.connectionInput ); } else if (this.distance >= 20) { context.document.connectToNewTarget( this.sourceObject.id, this.connectionOutput, context.viewState.transform.offsetToPagePoint(this.endPoint) ); } } context.document.onDragEnd(); } render(context: IFlowContext) { const transform = context.viewState.transform; const offsetRect = transform.clientToOffsetRect(transform.clientRect); const { lineShape } = generateConnectionLinePath( this.startPoint, this.sourceRect, this.endPoint, this.targetRect ); return ( ); } onTransformChanged(context: IFlowContext) { const startClientPoint = this.transform.offsetToPagePoint( this.startPoint ); this.startPoint = context.viewState.transform.pageToOffsetPoint(startClientPoint); super.onTransformChanged(context); } } export class NewConnectionLineFromInputMouseHandler extends MouseHandler { startPoint: Point; endPoint: Point; source: | { objectId: string; connectionOutput: string; } | undefined; cursor: string = "crosshair"; sourceRect: Rect | undefined; targetRect: Rect; constructor( private targetObject: TreeObjectAdapter, private connectionInput: string ) { super(); makeObservable(this, { startPoint: observable, endPoint: observable, source: observable.shallow, down: action, move: action, onTransformChanged: action }); } down(context: IFlowContext, event: IPointerEvent) { super.down(context, event); context.document.onDragStart(); const container = document.getElementById( context.viewState.containerId )!; const targetNode = container!.querySelector( `[data-eez-flow-object-id="${this.targetObject.id}"]` )!; const targetNodeBoundingClientRect = targetNode.getBoundingClientRect(); this.targetRect = context.viewState.transform.clientToOffsetRect( targetNodeBoundingClientRect ); const nodeInput = getConnectionInputNode( targetNode, this.connectionInput )!; const nodeInputBoundingClientRect = nodeInput.getBoundingClientRect(); const nodeInputPageRect = context.viewState.transform.clientToOffsetRect( nodeInputBoundingClientRect ); this.endPoint = { x: nodeInputPageRect.left, y: nodeInputPageRect.top + nodeInputPageRect.height / 2 }; this.startPoint = this.lastOffsetPoint; this.source = undefined; this.sourceRect = undefined; } move(context: IFlowContext, event: IPointerEvent) { super.move(context, event); const result = getObjectIdFromPoint( context.document, context.viewState, this.lastModelPoint ); if ( result && result.connectionOutput && !context.document.connectionExists( result.id, result.connectionOutput, this.targetObject.id, this.connectionInput ) ) { const container = document.getElementById( context.viewState.containerId )!; const sourceNode = container!.querySelector( `[data-eez-flow-object-id="${result.id}"]` )!; const sourceNodeBoundingClientRect = sourceNode.getBoundingClientRect(); this.sourceRect = context.viewState.transform.clientToOffsetRect( sourceNodeBoundingClientRect ); const nodeOutput = getConnectionOutputNode( sourceNode, result.connectionOutput )!; const nodeOutputBoundingClientRect = nodeOutput.getBoundingClientRect(); const nodeOutputPageRect = context.viewState.transform.clientToOffsetRect( nodeOutputBoundingClientRect ); this.startPoint = { x: nodeOutputPageRect.left + nodeOutputPageRect.width, y: nodeOutputPageRect.top + nodeOutputPageRect.height / 2 }; this.source = { objectId: result.id, connectionOutput: result.connectionOutput }; } else { this.startPoint = this.lastOffsetPoint; this.source = undefined; this.sourceRect = undefined; } } up(context: IFlowContext, cancel: boolean) { super.up(context, cancel); if (!cancel) { if (this.source) { context.document.connect( this.source.objectId, this.source.connectionOutput, this.targetObject.id, this.connectionInput ); } else if (this.distance >= 20) { context.document.connectToNewSource( this.targetObject.id, this.connectionInput, context.viewState.transform.offsetToPagePoint( this.startPoint ) ); } } context.document.onDragEnd(); } render(context: IFlowContext) { const transform = context.viewState.transform; const offsetRect = transform.clientToOffsetRect(transform.clientRect); const { lineShape } = generateConnectionLinePath( this.startPoint, this.sourceRect, this.endPoint, this.targetRect ); return ( ); } onTransformChanged(context: IFlowContext) { const endClientPoint = this.transform.offsetToPagePoint(this.endPoint); this.endPoint = context.viewState.transform.pageToOffsetPoint(endClientPoint); super.onTransformChanged(context); } } //////////////////////////////////////////////////////////////////////////////// export class MoveOutputConnectionLinesMouseHandler extends MouseHandler { connectionLines: ConnectionLine[] = []; startPoint: Point; source: | { objectId: string; connectionOutput: string; } | undefined; cursor: string = "crosshair"; sourceRect: Rect | undefined; constructor( private sourceObject: TreeObjectAdapter, private connectionOutput: string ) { super(); makeObservable(this, { connectionLines: observable, startPoint: observable, source: observable.shallow, down: action, move: action }); } down(context: IFlowContext, event: IPointerEvent) { super.down(context, event); context.document.onDragStart(); // get selected connection lines, but only if all selected objects are only connection lines not components let selectedConnectionLines: ConnectionLine[]; if ( context.document.selectedConnectionLines.length > 0 && !context.document.selectedConnectionLines.find( objectAdapter => !( objectAdapter.object instanceof ProjectEditor.ConnectionLineClass ) ) ) { selectedConnectionLines = context.document.selectedConnectionLines.map( objectAdapter => objectAdapter.object as ConnectionLine ); } else { selectedConnectionLines = []; } if ( selectedConnectionLines.length > 0 && !selectedConnectionLines.find( connectionLine => connectionLine.sourceComponent != this.sourceObject.object || connectionLine.output != this.connectionOutput ) ) { // All selected connection lines are connected with (this.sourceObject, this.connectionOutput). // Work only with these lines. this.connectionLines = selectedConnectionLines; } else { // Work with all connection lines connected to (this.sourceObject, this.connectionOutput). this.connectionLines = ( context.document.flow.object as Flow ).connectionLines.filter( connectionLine => connectionLine.sourceComponent == this.sourceObject.object && connectionLine.output == this.connectionOutput ); } context.viewState.selectObjects( this.connectionLines.map( connectionLine => context.document.findObjectById(getId(connectionLine))! ) ); this.startPoint = this.lastOffsetPoint; this.source = undefined; this.sourceRect = undefined; } move(context: IFlowContext, event: IPointerEvent) { super.move(context, event); const result = getObjectIdFromPoint( context.document, context.viewState, this.lastModelPoint ); if ( result && result.connectionOutput && (result.id != this.sourceObject.id || result.connectionOutput != this.connectionOutput) ) { const container = document.getElementById( context.viewState.containerId )!; const sourceNode = container!.querySelector( `[data-eez-flow-object-id="${result.id}"]` )!; const sourceNodeBoundingClientRect = sourceNode.getBoundingClientRect(); this.sourceRect = context.viewState.transform.clientToOffsetRect( sourceNodeBoundingClientRect ); const nodeOutput = getConnectionOutputNode( sourceNode, result.connectionOutput )!; const nodeOutputBoundingClientRect = nodeOutput.getBoundingClientRect(); const nodeOutputPageRect = context.viewState.transform.clientToOffsetRect( nodeOutputBoundingClientRect ); this.startPoint = { x: nodeOutputPageRect.left + nodeOutputPageRect.width, y: nodeOutputPageRect.top + nodeOutputPageRect.height / 2 }; this.source = { objectId: result.id, connectionOutput: result.connectionOutput }; } else { this.startPoint = this.lastOffsetPoint; this.source = undefined; this.sourceRect = undefined; } } up(context: IFlowContext, cancel: boolean) { super.up(context, cancel); if (!cancel) { const source = this.source; if (source) { const sourceObject = context.projectStore.getObjectFromObjectId( source.objectId ) as Component; const changes = { source: sourceObject.objID, output: source.connectionOutput }; if (this.connectionLines.length > 0) { context.projectStore.undoManager.setCombineCommands(true); this.connectionLines.forEach(connectionLine => { if ( context.document.connectionExists( source.objectId, source.connectionOutput, getId(connectionLine.targetComponent!), connectionLine.input ) ) { context.projectStore.deleteObject(connectionLine); } else { context.projectStore.updateObject( connectionLine, changes ); } }); context.projectStore.undoManager.setCombineCommands(false); } context.viewState.deselectAllObjects(); } } context.document.onDragEnd(); } render(context: IFlowContext) { const transform = context.viewState.transform; const offsetRect = transform.clientToOffsetRect(transform.clientRect); return ( ); } } //////////////////////////////////////////////////////////////////////////////// export class MoveInputConnectionLinesMouseHandler extends MouseHandler { connectionLines: ConnectionLine[] = []; endPoint: Point; target: | { objectId: string; connectionInput: string; } | undefined; cursor: string = "crosshair"; targetRect: Rect | undefined; constructor( private targetObject: TreeObjectAdapter, private connectionInput: string ) { super(); makeObservable(this, { connectionLines: observable, endPoint: observable, target: observable.shallow, down: action, move: action }); } down(context: IFlowContext, event: IPointerEvent) { super.down(context, event); context.document.onDragStart(); // get selected connection lines, but only if all selected objects are only connection lines not components let selectedConnectionLines: ConnectionLine[]; if ( context.document.selectedConnectionLines.length > 0 && !context.document.selectedConnectionLines.find( objectAdapter => !( objectAdapter.object instanceof ProjectEditor.ConnectionLineClass ) ) ) { selectedConnectionLines = context.document.selectedConnectionLines.map( objectAdapter => objectAdapter.object as ConnectionLine ); } else { selectedConnectionLines = []; } if ( selectedConnectionLines.length > 0 && !selectedConnectionLines.find( connectionLine => connectionLine.targetComponent != this.targetObject.object || connectionLine.input != this.connectionInput ) ) { // All selected connection lines are connected with (this.targetObject, this.connectionInput). // Work only with these lines. this.connectionLines = selectedConnectionLines; } else { // Work with all connection lines connected to (this.targetObject, this.connectionInput). this.connectionLines = ( context.document.flow.object as Flow ).connectionLines.filter( connectionLine => connectionLine.targetComponent == this.targetObject.object && connectionLine.input == this.connectionInput ); } context.viewState.selectObjects( this.connectionLines.map( connectionLine => context.document.findObjectById(getId(connectionLine))! ) ); this.endPoint = this.lastOffsetPoint; this.target = undefined; this.targetRect = undefined; } move(context: IFlowContext, event: IPointerEvent) { super.move(context, event); const result = getObjectIdFromPoint( context.document, context.viewState, this.lastModelPoint ); if ( result && result.connectionInput && (result.id != this.targetObject.id || result.connectionInput != this.connectionInput) ) { const container = document.getElementById( context.viewState.containerId )!; const targetNode = container!.querySelector( `[data-eez-flow-object-id="${result.id}"]` )!; const targetNodeBoundingClientRect = targetNode.getBoundingClientRect(); this.targetRect = context.viewState.transform.clientToOffsetRect( targetNodeBoundingClientRect ); const nodeInput = getConnectionInputNode( targetNode, result.connectionInput )!; const nodeInputBoundingClientRect = nodeInput.getBoundingClientRect(); const nodeInputPageRect = context.viewState.transform.clientToOffsetRect( nodeInputBoundingClientRect ); this.endPoint = { x: nodeInputPageRect.left, y: nodeInputPageRect.top + nodeInputPageRect.height / 2 }; this.target = { objectId: result.id, connectionInput: result.connectionInput }; } else { this.endPoint = this.lastOffsetPoint; this.target = undefined; this.targetRect = undefined; } } up(context: IFlowContext, cancel: boolean) { super.up(context, cancel); if (!cancel) { const target = this.target; if (target) { const targetObject = context.projectStore.getObjectFromObjectId( target.objectId ) as Component; const changes = { target: targetObject.objID, input: target.connectionInput }; if (this.connectionLines.length > 0) { context.projectStore.undoManager.setCombineCommands(true); this.connectionLines.forEach(connectionLine => { if ( context.document.connectionExists( getId(connectionLine.sourceComponent!), connectionLine.output, target.objectId, target.connectionInput ) ) { context.projectStore.deleteObject(connectionLine); } else { context.projectStore.updateObject( connectionLine, changes ); } }); context.projectStore.undoManager.setCombineCommands(false); } context.viewState.deselectAllObjects(); } } context.document.onDragEnd(); } render(context: IFlowContext) { const transform = context.viewState.transform; const offsetRect = transform.clientToOffsetRect(transform.clientRect); return ( ); } } function getConnectionOutputNode( sourceNode: Element, connectionOutput: string ) { const outputNodes = sourceNode.querySelectorAll( `[data-connection-output-id="${connectionOutput}"]` )!; for (let i = 0; i < outputNodes.length; i++) { const outputNode = outputNodes[i]; if (outputNode.closest(".EezStudio_ComponentEnclosure") == sourceNode) { return outputNode; } } return undefined; } function getConnectionInputNode(targetNode: Element, connectionInput: string) { const inputNodes = targetNode.querySelectorAll( `[data-connection-input-id="${connectionInput}"]` )!; for (let i = 0; i < inputNodes.length; i++) { const outputNode = inputNodes[i]; if (outputNode.closest(".EezStudio_ComponentEnclosure") == targetNode) { return outputNode; } } return undefined; }