Skip to content

Commit ab2b5ef

Browse files
authored
feat: introduce DragSystem (#198)
1 parent aa6dadd commit ab2b5ef

File tree

25 files changed

+1410
-288
lines changed

25 files changed

+1410
-288
lines changed

docs/components/canvas-graph-component.md

Lines changed: 110 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ classDiagram
1414
GraphComponent <|-- Connection
1515
GraphComponent -- HitBox
1616
GraphComponent -- Camera
17+
GraphComponent -- DragService
1718
1819
class EventedComponent {
1920
+props: TComponentProps
@@ -29,11 +30,15 @@ classDiagram
2930
+getHitBox()
3031
+isVisible()
3132
+onDrag()
33+
+isDraggable()
34+
+handleDragStart()
35+
+handleDrag()
36+
+handleDragEnd()
3237
+subscribeSignal()
3338
}
3439
```
3540

36-
## The Four Core Capabilities
41+
## The Core Capabilities
3742

3843
### 1. Spatial Awareness with HitBox and R-tree
3944

@@ -187,6 +192,110 @@ onDragUpdate: (diff: {
187192
- Use `diffX`/`diffY` when you need to calculate position relative to drag start (e.g., `initialPosition + diffX`)
188193
- Use `deltaX`/`deltaY` when you need frame-to-frame movement (e.g., `currentPosition + deltaX`)
189194

195+
### 3.1 DragService Integration
196+
197+
For components that need to participate in the centralized drag system (multi-selection drag, autopanning, etc.), GraphComponent provides lifecycle methods that integrate with [DragService](../system/drag-system.md).
198+
199+
> **Note:** The `onDrag()` method is for simple, self-contained drag behavior. For components that need to work with multi-selection and the centralized drag system, override the `isDraggable()` and `handleDrag*()` methods instead.
200+
201+
**Key differences:**
202+
| Feature | `onDrag()` method | DragService methods |
203+
|---------|-------------------|---------------------|
204+
| Multi-selection support | ❌ No | ✅ Yes |
205+
| Autopanning | Manual setup | ✅ Automatic |
206+
| Cursor management | Manual setup | ✅ Automatic |
207+
| Drag state tracking | ❌ No | ✅ Via `$state` signal |
208+
| Use case | Simple standalone drag | Blocks, groups, custom entities |
209+
210+
**DragService lifecycle methods:**
211+
212+
```typescript
213+
import { GraphComponent, DragContext, DragDiff } from "@gravity-ui/graph";
214+
215+
class MyDraggableComponent extends GraphComponent {
216+
private initialPosition: { x: number; y: number } | null = null;
217+
218+
/**
219+
* Return true to enable dragging for this component.
220+
* Components returning true will participate in DragService-managed operations.
221+
*/
222+
public override isDraggable(): boolean {
223+
return !this.props.locked;
224+
}
225+
226+
/**
227+
* Called when drag operation starts.
228+
* Use this to store initial state needed for drag calculations.
229+
*/
230+
public override handleDragStart(context: DragContext): void {
231+
this.initialPosition = {
232+
x: this.state.x,
233+
y: this.state.y,
234+
};
235+
}
236+
237+
/**
238+
* Called on each frame during drag.
239+
* Update component position based on diff values.
240+
*/
241+
public override handleDrag(diff: DragDiff, context: DragContext): void {
242+
if (!this.initialPosition) return;
243+
244+
// Option 1: Use absolute diff (stable positioning)
245+
const newX = this.initialPosition.x + diff.diffX;
246+
const newY = this.initialPosition.y + diff.diffY;
247+
248+
// Option 2: Use incremental delta (frame-to-frame)
249+
// const newX = this.state.x + diff.deltaX;
250+
// const newY = this.state.y + diff.deltaY;
251+
252+
this.setState({ x: newX, y: newY });
253+
this.updateHitBox();
254+
}
255+
256+
/**
257+
* Called when drag operation ends.
258+
* Use this to finalize state and clean up.
259+
*/
260+
public override handleDragEnd(context: DragContext): void {
261+
this.initialPosition = null;
262+
this.updateHitBox();
263+
}
264+
}
265+
```
266+
267+
**DragContext properties:**
268+
269+
| Property | Type | Description |
270+
|----------|------|-------------|
271+
| `sourceEvent` | `MouseEvent` | The native mouse event |
272+
| `startCoords` | `[number, number]` | World coordinates when drag started |
273+
| `prevCoords` | `[number, number]` | World coordinates from previous frame |
274+
| `currentCoords` | `[number, number]` | Current world coordinates |
275+
| `components` | `GraphComponent[]` | All components participating in this drag |
276+
277+
**Checking drag state from anywhere:**
278+
279+
```typescript
280+
// Access drag state via DragService
281+
const dragState = graph.dragService.$state.value;
282+
283+
if (dragState.isDragging) {
284+
console.log("Components being dragged:", dragState.components.length);
285+
console.log("Is homogeneous drag:", dragState.isHomogeneous);
286+
console.log("Component types:", [...dragState.componentTypes]);
287+
}
288+
289+
// Subscribe to drag state changes
290+
graph.dragService.$state.subscribe((state) => {
291+
if (state.isDragging && !state.isHomogeneous) {
292+
// Disable snapping for heterogeneous multi-select drag
293+
}
294+
});
295+
```
296+
297+
For more details on the drag system, see [Drag System](../system/drag-system.md).
298+
190299
### 4. Reactive Data with Signal Subscriptions
191300

192301
GraphComponent enables reactive programming with a simple subscription system:

0 commit comments

Comments
 (0)