From d815a32eb385e89fdd022681778bbcf34a53c3e5 Mon Sep 17 00:00:00 2001 From: draedful Date: Tue, 7 Jul 2026 13:52:38 +0300 Subject: [PATCH 1/2] fix(camera): restore pan on non-draggable graph components Allow camera pan when clicking blocks with canDrag disabled by deferring only to GraphComponent.isDraggable(), matching DragService behavior. Co-authored-by: Cursor --- src/services/camera/Camera.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/services/camera/Camera.ts b/src/services/camera/Camera.ts index 1dfdfc9f..839a567c 100644 --- a/src/services/camera/Camera.ts +++ b/src/services/camera/Camera.ts @@ -1,4 +1,5 @@ import { EventedComponent } from "../../components/canvas/EventedComponent/EventedComponent"; +import { GraphComponent } from "../../components/canvas/GraphComponent"; import { TGraphLayerContext } from "../../components/canvas/layers/graphLayer/GraphLayer"; import { GraphMouseEvent } from "../../graphEvents"; import { Component, ESchedulerPriority } from "../../lib"; @@ -189,9 +190,7 @@ export class Camera extends EventedComponent this.onDragEnd()); }; + /** + * DragService handles draggable graph components; camera pan is deferred in that case. + */ + private isTargetDraggable(target: EventedComponent | undefined): boolean { + return target instanceof GraphComponent && target.isDraggable(); + } + private onDragStart(event: MouseEvent) { this.lastDragEvent = event; } From c92d064f3f539c4e1915ee91ce7d914b3fd514e0 Mon Sep 17 00:00:00 2001 From: draedful Date: Tue, 7 Jul 2026 15:21:52 +0300 Subject: [PATCH 2/2] fix(input): coordinate camera pan with connection layer mousedown handling Respect preventGraphEventDefault from connection layers, restrict connection drag to left-click and snappable ports, and avoid blocking block drag in PortConnectionLayer when the pointer is not on a port. Co-authored-by: Cursor --- .../layers/connectionLayer/ConnectionLayer.ts | 6 ++- .../PortConnectionLayer.ts | 46 ++++++++++++------- src/services/camera/Camera.ts | 5 +- 3 files changed, 38 insertions(+), 19 deletions(-) diff --git a/src/components/canvas/layers/connectionLayer/ConnectionLayer.ts b/src/components/canvas/layers/connectionLayer/ConnectionLayer.ts index f56df6a7..f523398a 100644 --- a/src/components/canvas/layers/connectionLayer/ConnectionLayer.ts +++ b/src/components/canvas/layers/connectionLayer/ConnectionLayer.ts @@ -159,7 +159,7 @@ export class ConnectionLayer extends Layer< */ protected afterInit(): void { // Register event listeners with the graphOn wrapper method for automatic cleanup when unmounted - this.onGraphEvent("mousedown", this.handleMouseDown); + this.onGraphEvent("mousedown", this.handleMouseDown, { capture: true }); // Call parent afterInit to ensure proper initialization super.afterInit(); @@ -196,12 +196,16 @@ export class ConnectionLayer extends Layer< if (!initEvent || !target || !this.root?.ownerDocument) { return; } + if (initEvent.button !== 0) { + return; + } if ( this.checkIsShouldStartCreationConnection(target as GraphComponent, initEvent) && (isBlock(target) || target instanceof Anchor) ) { if (isGraphEvent(nativeEvent)) { + nativeEvent.preventGraphEventDefault(); nativeEvent.stopGraphEventPropagation(); } this.context.graph.dragService.startDrag( diff --git a/src/components/canvas/layers/portConnectionLayer/PortConnectionLayer.ts b/src/components/canvas/layers/portConnectionLayer/PortConnectionLayer.ts index e8ab4323..3a832daf 100644 --- a/src/components/canvas/layers/portConnectionLayer/PortConnectionLayer.ts +++ b/src/components/canvas/layers/portConnectionLayer/PortConnectionLayer.ts @@ -8,7 +8,7 @@ import { EAnchorType } from "../../../../store/anchor/Anchor"; import { TBlockId } from "../../../../store/block/Block"; import { PortState } from "../../../../store/connection/port/Port"; import type { Emitter } from "../../../../utils/Emitter"; -import { vectorDistance } from "../../../../utils/functions"; +import { getXY, vectorDistance } from "../../../../utils/functions"; import { stopDragListening } from "../../../../utils/functions/dragListener"; import { render } from "../../../../utils/renderers/render"; import { renderSVG } from "../../../../utils/renderers/svgPath"; @@ -260,6 +260,11 @@ export class PortConnectionLayer extends Layer< protected currentListener: Emitter | null = null; + private isSnappablePort(port: PortState): boolean { + const meta = port.meta?.[PortConnectionLayer.PortMetaKey] as IPortConnectionMeta | undefined; + return Boolean(meta?.snappable); + } + protected handleMouseDown = (nativeEvent: GraphMouseEvent): void => { if (!this.enabled) { return; @@ -269,27 +274,36 @@ export class PortConnectionLayer extends Layer< if (!initEvent || !this.root?.ownerDocument || !initialComponent) { return; } + if (initEvent.button !== 0) { + return; + } if (!(initialComponent instanceof GraphComponent) || initialComponent.getPorts().length === 0) { return; } + + const canvas = this.context.graph.getGraphCanvas(); + const [screenX, screenY] = getXY(canvas, initEvent); + const [worldX, worldY] = this.context.graph.cameraService.applyToPoint(screenX, screenY); + const searchRadius = this.props.searchRadius || PORT_SEARCH_RADIUS; + const port = this.context.graph.rootStore.connectionsList.ports.findPortAtPointByComponent( + initialComponent, + new Point(worldX, worldY), + searchRadius, + (candidate) => this.isSnappablePort(candidate) + ); + if (!port) { + return; + } + if (isGraphEvent(nativeEvent)) { + nativeEvent.preventGraphEventDefault(); nativeEvent.stopGraphEventPropagation(); } // DragService will provide world coordinates in callbacks this.currentListener = this.context.graph.dragService.startDrag( { onStart: (_event, coords) => { - const point = new Point(coords[0], coords[1]); - const searchRadius = this.props.searchRadius || PORT_SEARCH_RADIUS; - - const port = this.context.graph.rootStore.connectionsList.ports.findPortAtPointByComponent( - initialComponent, - point, - searchRadius - ); - if (port) { - this.onStartConnection(port, point); - } + this.onStartConnection(port, new Point(coords[0], coords[1])); }, onUpdate: (event, coords) => this.onMoveNewConnection(event, new Point(coords[0], coords[1])), onEnd: (_event, coords) => this.onEndNewConnection(new Point(coords[0], coords[1])), @@ -410,7 +424,7 @@ export class PortConnectionLayer extends Layer< // Try to find port at cursor without snapping const searchRadius = this.props.searchRadius || PORT_SEARCH_RADIUS; newTargetPort = this.context.graph.rootStore.connectionsList.ports.findPortAtPoint(point, searchRadius, (p) => { - return Boolean(p.owner) && p.id !== this.sourcePort?.id; + return this.isSnappablePort(p) && Boolean(p.owner) && p.id !== this.sourcePort?.id; }); } @@ -498,7 +512,7 @@ export class PortConnectionLayer extends Layer< // Fallback: try to find port at drop point const searchRadius = this.props.searchRadius || PORT_SEARCH_RADIUS; targetPort = this.context.graph.rootStore.connectionsList.ports.findPortAtPoint(point, searchRadius, (p) => { - return Boolean(p.owner) && p.id !== this.sourcePort?.id; + return this.isSnappablePort(p) && Boolean(p.owner) && p.id !== this.sourcePort?.id; }); } @@ -681,9 +695,7 @@ export class PortConnectionLayer extends Layer< // Skip ports in lookup state (no valid coordinates) if (port.lookup) continue; - const meta = port.meta?.[PortConnectionLayer.PortMetaKey] as IPortConnectionMeta | undefined; - - if (meta?.snappable) { + if (this.isSnappablePort(port)) { snappingBoxes.push({ minX: port.x - searchRadius, minY: port.y - searchRadius, diff --git a/src/services/camera/Camera.ts b/src/services/camera/Camera.ts index 839a567c..45c2da57 100644 --- a/src/services/camera/Camera.ts +++ b/src/services/camera/Camera.ts @@ -1,7 +1,7 @@ import { EventedComponent } from "../../components/canvas/EventedComponent/EventedComponent"; import { GraphComponent } from "../../components/canvas/GraphComponent"; import { TGraphLayerContext } from "../../components/canvas/layers/graphLayer/GraphLayer"; -import { GraphMouseEvent } from "../../graphEvents"; +import { GraphMouseEvent, isGraphEvent } from "../../graphEvents"; import { Component, ESchedulerPriority } from "../../lib"; import { TComponentProps, TComponentState } from "../../lib/Component"; import { ComponentDescriptor } from "../../lib/CoreComponent"; @@ -182,6 +182,9 @@ export class Camera extends EventedComponent