Skip to content

Commit efadf44

Browse files
authored
fix: fix creating connections via ConnectionLayer (#218)
fix(ConnectionLayer): fix creating connection after preventDefault() fix(CursorLayer): Add debounce to set cursor fix(Anchor): fix calculation hitbox of achor on hover
1 parent 5bad678 commit efadf44

File tree

5 files changed

+101
-17
lines changed

5 files changed

+101
-17
lines changed

src/components/canvas/anchors/index.ts

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { AnchorState, EAnchorType } from "../../../store/anchor/Anchor";
44
import { TBlockId } from "../../../store/block/Block";
55
import { selectBlockAnchor } from "../../../store/block/selectors";
66
import { PortState } from "../../../store/connection/port/Port";
7-
import { TPoint } from "../../../utils/types/shapes";
87
import { GraphComponent, TGraphComponentProps } from "../GraphComponent";
98
import { GraphLayer } from "../layers/graphLayer/GraphLayer";
109

@@ -33,14 +32,17 @@ type TAnchorState = {
3332
export class Anchor<T extends TAnchorProps = TAnchorProps> extends GraphComponent<T, TAnchorState> {
3433
public readonly cursor = "pointer";
3534

35+
public static CANVAS_HOVER_FACTOR = 1.8;
36+
public static DETAILED_HOVER_FACTOR = 1.2;
37+
3638
public get zIndex() {
3739
// @ts-ignore this.__comp.parent instanceOf Block
3840
return this.__comp.parent.zIndex + 1;
3941
}
4042

4143
public connectedState: AnchorState;
4244

43-
private shift: number;
45+
private shift = 0;
4446

4547
constructor(props: T, parent: GraphLayer) {
4648
super(props, parent);
@@ -55,7 +57,29 @@ export class Anchor<T extends TAnchorProps = TAnchorProps> extends GraphComponen
5557
this.addEventListener("mouseleave", this);
5658

5759
this.computeRenderSize(this.props.size, this.state.raised);
58-
this.shift = this.props.size / 2 + props.lineWidth;
60+
}
61+
62+
public getHoverFactor() {
63+
if (this.context.camera.getCameraBlockScaleLevel() === ECameraScaleLevel.Detailed) {
64+
return Anchor.DETAILED_HOVER_FACTOR;
65+
}
66+
return Anchor.CANVAS_HOVER_FACTOR;
67+
}
68+
69+
protected stateChanged(_nextState: TAnchorState): void {
70+
if (this.state.size !== _nextState.size) {
71+
this.computeShift(_nextState, this.props);
72+
this.onPositionChanged();
73+
}
74+
super.stateChanged(_nextState);
75+
}
76+
77+
protected propsChanged(_nextProps: T): void {
78+
if (this.props.lineWidth !== _nextProps.lineWidth) {
79+
this.computeShift(this.state, _nextProps);
80+
this.onPositionChanged();
81+
}
82+
super.propsChanged(_nextProps);
5983
}
6084

6185
protected willMount(): void {
@@ -64,10 +88,18 @@ export class Anchor<T extends TAnchorProps = TAnchorProps> extends GraphComponen
6488
this.setState({ selected });
6589
});
6690
this.subscribeSignal(this.props.port.$point, this.onPositionChanged);
91+
this.computeShift(this.state, this.props);
92+
this.onPositionChanged();
93+
super.willMount();
6794
}
6895

69-
protected onPositionChanged = (point: TPoint) => {
70-
this.setHitBox(point.x - this.shift, point.y - this.shift, point.x + this.shift, point.y + this.shift);
96+
protected computeShift(state = this.state, props = this.props) {
97+
this.shift = state.size / 2 + props.lineWidth;
98+
}
99+
100+
protected onPositionChanged = () => {
101+
const { x, y } = this.getPosition();
102+
this.setHitBox(x - this.shift, y - this.shift, x + this.shift, y + this.shift);
71103
};
72104

73105
public getPosition() {
@@ -139,7 +171,7 @@ export class Anchor<T extends TAnchorProps = TAnchorProps> extends GraphComponen
139171

140172
private computeRenderSize(size: number, raised: boolean) {
141173
if (raised) {
142-
this.setState({ size: size * 1.8 });
174+
this.setState({ size: size * this.getHoverFactor() });
143175
} else {
144176
this.setState({ size });
145177
}

src/components/canvas/connections/BaseConnection.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { Component } from "../../../lib";
1+
import { Component, ESchedulerPriority } from "../../../lib";
22
import { TComponentState } from "../../../lib/Component";
33
import { ConnectionState, TConnection, TConnectionId } from "../../../store/connection/ConnectionState";
44
import { selectConnectionById } from "../../../store/connection/selectors";
5+
import { debounce } from "../../../utils/functions";
56
import { TPoint } from "../../../utils/types/shapes";
67
import { GraphComponent, GraphComponentContext } from "../GraphComponent";
78
import { TAnchor } from "../anchors";
@@ -168,14 +169,32 @@ export class BaseConnection<
168169

169170
switch (event.type) {
170171
case "mouseenter":
171-
this.setState({ hovered: true });
172+
this.debounceHover(true);
172173
break;
173174
case "mouseleave":
174-
this.setState({ hovered: false });
175+
this.debounceHover(false);
175176
break;
176177
}
177178
}
178179

180+
protected onHoverChange(hoverState: boolean) {
181+
if (hoverState === this.state.hovered) {
182+
return;
183+
}
184+
this.setState({ hovered: hoverState });
185+
}
186+
187+
protected debounceHover = debounce(
188+
(hovered: boolean) => {
189+
this.onHoverChange(hovered);
190+
},
191+
{
192+
priority: ESchedulerPriority.LOWEST,
193+
frameInterval: 3,
194+
frameTimeout: 0,
195+
}
196+
);
197+
179198
protected override unmount(): void {
180199
this.connectedState.$sourcePortState.value.removeObserver(this);
181200
this.connectedState.$targetPortState.value.removeObserver(this);

src/components/canvas/layers/connectionLayer/ConnectionLayer.ts

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,13 @@ export class ConnectionLayer extends Layer<
295295
if (!sourceComponent) {
296296
return;
297297
}
298+
this.sourceComponent = sourceComponent.connectedState;
299+
if (sourceComponent instanceof Block) {
300+
this.startState = new Point(worldCoords.x, worldCoords.y);
301+
} else if (sourceComponent instanceof Anchor) {
302+
const point = sourceComponent.getPosition();
303+
this.startState = new Point(point.x, point.y);
304+
}
298305

299306
this.context.graph.executеDefaultEventAction(
300307
"connection-create-start",
@@ -306,17 +313,11 @@ export class ConnectionLayer extends Layer<
306313
anchorId: sourceComponent instanceof Anchor ? sourceComponent.connectedState.id : undefined,
307314
},
308315
() => {
309-
this.sourceComponent = sourceComponent.connectedState;
310316
if (sourceComponent instanceof Block) {
311317
this.context.graph.api.selectBlocks([this.sourceComponent.id], true, ESelectionStrategy.REPLACE);
312-
this.startState = new Point(worldCoords.x, worldCoords.y);
313318
} else if (sourceComponent instanceof Anchor) {
314-
const point = sourceComponent.getPosition();
315-
this.startState = new Point(point.x, point.y);
316319
this.context.graph.api.setAnchorSelection(sourceComponent.props.blockId, sourceComponent.props.id, true);
317320
}
318-
319-
// Use world coordinates from point instead of screen coordinates
320321
}
321322
);
322323

src/components/canvas/layers/cursorLayer/CursorLayer.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { Graph } from "../../../../graph";
2+
import { ESchedulerPriority } from "../../../../lib";
23
import { Layer, LayerContext, LayerProps } from "../../../../services/Layer";
4+
import { debounce } from "../../../../utils/functions";
35
import { EventedComponent } from "../../EventedComponent/EventedComponent";
46
import { GraphComponent } from "../../GraphComponent";
57

@@ -110,6 +112,9 @@ export class CursorLayer extends Layer<TCursorLayerProps, TCursorLayerContext> {
110112
/** Component currently under the mouse cursor */
111113
private currentTarget?: GraphComponent;
112114

115+
/** Debounced cursor update function to prevent flickering on large scales */
116+
private debouncedUpdateCursor: ReturnType<typeof debounce<(target?: EventedComponent) => void>>;
117+
113118
/**
114119
* Creates a new CursorLayer instance.
115120
*
@@ -120,6 +125,17 @@ export class CursorLayer extends Layer<TCursorLayerProps, TCursorLayerContext> {
120125
// No HTML element needed - we'll apply cursor to the root element
121126
...props,
122127
});
128+
129+
// Create debounced version with 10 frames delay to prevent cursor flickering
130+
this.debouncedUpdateCursor = debounce(
131+
(target?: EventedComponent) => {
132+
this.updateCursorForTarget(target);
133+
},
134+
{
135+
frameInterval: 3,
136+
priority: ESchedulerPriority.LOW,
137+
}
138+
);
123139
}
124140

125141
/**
@@ -152,11 +168,15 @@ export class CursorLayer extends Layer<TCursorLayerProps, TCursorLayerContext> {
152168

153169
// Apply cursor only in automatic mode
154170
if (this.mode === "auto") {
155-
this.updateCursorForTarget(target);
171+
// Use debounced version to prevent cursor flickering on large scales
172+
this.debouncedUpdateCursor(target);
156173
}
157174
});
158175

159176
this.onGraphEvent("mouseleave", () => {
177+
// Cancel any scheduled cursor updates from mouseenter
178+
this.debouncedUpdateCursor.cancel();
179+
160180
// Always track that mouse left the element
161181
this.currentTarget = undefined;
162182

@@ -195,6 +215,9 @@ export class CursorLayer extends Layer<TCursorLayerProps, TCursorLayerContext> {
195215
}
196216

197217
protected unmountLayer(): void {
218+
// Cancel any pending cursor updates
219+
this.debouncedUpdateCursor.cancel();
220+
198221
super.unmountLayer();
199222
const rootElement = this.props.graph.layers.$root;
200223
if (rootElement) {

src/react-components/Anchor.tsx

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo } from "react";
1+
import React, { useEffect, useMemo } from "react";
22

33
import { TAnchor } from "../components/canvas/anchors";
44
import { Graph } from "../graph";
@@ -36,6 +36,15 @@ export function GraphBlockAnchor({
3636
} ${selected ? "graph-block-anchor-selected" : ""}`;
3737
}, [anchor, position, className, selected]);
3838

39+
useEffect(() => {
40+
if (anchorContainerRef.current) {
41+
const hoverScale = anchorState.getViewComponent().getHoverFactor();
42+
if (hoverScale !== undefined) {
43+
anchorContainerRef.current.style.setProperty("--graph-block-anchor-hover-scale", hoverScale.toString());
44+
}
45+
}
46+
}, [anchorState?.$selected.value]);
47+
3948
if (!anchorState) return null;
4049

4150
return (

0 commit comments

Comments
 (0)