Skip to content

Commit 096543a

Browse files
authored
feat: Presented BlockGroups layer (#48)
1 parent a3a181a commit 096543a

File tree

21 files changed

+1372
-21
lines changed

21 files changed

+1372
-21
lines changed

docs/groups.md

Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
# Working with Groups
2+
3+
Groups in the library provide a way to organize and manage collections of blocks. They support visual styling, behavior configuration, and geometric properties.
4+
5+
## How to Use Groups
6+
7+
There are two ways to work with groups:
8+
9+
### 1. Automatic Groups
10+
11+
Use this when you want groups to update automatically based on block properties. For example, you can group blocks by their color or type.
12+
13+
```typescript
14+
// Make groups automatically
15+
const AutoGroups = BlockGroups.withBlockGrouping({
16+
// Put blocks in groups
17+
groupingFn: (blocks) => {
18+
const groups = {};
19+
blocks.forEach(block => {
20+
const groupId = block.type; // or any other property
21+
if (!groups[groupId]) groups[groupId] = [];
22+
groups[groupId].push(block);
23+
});
24+
return groups;
25+
},
26+
// Set how groups look
27+
mapToGroups: (groupId, { rect }) => ({
28+
id: groupId,
29+
rect,
30+
style: {
31+
background: "rgba(0, 100, 200, 0.1)"
32+
}
33+
})
34+
});
35+
36+
// Add groups to graph
37+
graph.addLayer(AutoGroups, {
38+
draggable: true, // groups can be moved
39+
updateBlocksOnDrag: true // blocks move with group. Works only if group is draggable and used Automatic Groups
40+
});
41+
```
42+
43+
### 2. Manual Groups
44+
45+
Use direct `BlockGroups` methods when you need manual control over groups. This approach is useful when:
46+
- Groups are created/updated based on user actions
47+
- You need custom group management logic
48+
- Groups are independent of block properties
49+
50+
```typescript
51+
// Add groups to graph
52+
const groups = graph.addLayer(BlockGroups, {
53+
draggable: true,
54+
});
55+
56+
// Set groups
57+
blockGroups.setGroups([
58+
{
59+
id: "group1",
60+
rect: { x: 0, y: 0, width: 100, height: 100 },
61+
style: {
62+
background: "rgba(0, 100, 200, 0.1)"
63+
}
64+
}
65+
]);
66+
67+
// Update specific groups
68+
blockGroups.updateGroups([
69+
{
70+
id: "group1",
71+
rect: { x: 50, y: 50, width: 100, height: 100 }
72+
}
73+
]);
74+
```
75+
76+
## Basic Usage
77+
78+
### Creating a Group
79+
80+
```typescript
81+
// Add fixed area groups to graph
82+
const areas = graph.addLayer(BlockGroups, {
83+
draggable: false, // areas cannot be moved
84+
});
85+
86+
// Create fixed areas
87+
areas.setGroups([
88+
{
89+
id: "area1",
90+
rect: { x: 0, y: 0, width: 800, height: 400 },
91+
style: {
92+
background: "rgba(100, 149, 237, 0.1)",
93+
border: "rgba(100, 149, 237, 0.3)"
94+
}
95+
},
96+
{
97+
id: "area2",
98+
rect: { x: 800, y: 0, width: 800, height: 400 },
99+
style: {
100+
background: "rgba(144, 238, 144, 0.1)",
101+
border: "rgba(144, 238, 144, 0.3)"
102+
}
103+
}
104+
]);
105+
```
106+
107+
Important: Dragging is not yet implemented properly for fixed area groups. Always set `draggable: false` for such groups. They should remain fixed while blocks can move freely between them.
108+
109+
## Group Settings
110+
111+
### Style
112+
You can change how groups look:
113+
```typescript
114+
{
115+
background: "rgba(100, 100, 100, 0.1)", // group color
116+
border: "rgba(100, 100, 100, 0.3)", // border color
117+
borderWidth: 2, // border thickness
118+
selectedBackground: "rgba(100, 100, 100, 1)", // color when selected
119+
selectedBorder: "rgba(100, 100, 100, 1)", // border when selected
120+
}
121+
```
122+
123+
### Size and Space
124+
You can set space around blocks in group:
125+
```typescript
126+
{
127+
padding: [20, 20, 20, 20] // space at [top, right, bottom, left]
128+
}
129+
```
130+
131+
### Behavior
132+
You can control how groups work:
133+
```typescript
134+
{
135+
draggable: true, // can move group with mouse
136+
updateBlocksOnDrag: true // blocks move with group. Works only if group is draggable and used Automatic Groups
137+
}
138+
```
139+
140+
## API Reference
141+
142+
### BlockGroups Methods
143+
```typescript
144+
// Create or replace all groups
145+
setGroups(groups: TGroup[]): void;
146+
147+
// Update some groups
148+
updateGroups(groups: TGroup[]): void;
149+
```
150+
151+
### Group Properties
152+
```typescript
153+
// What you can set in a group
154+
{
155+
id: string; // unique group ID
156+
rect: { // where and how big
157+
x: number;
158+
y: number;
159+
width: number;
160+
height: number;
161+
};
162+
selected?: boolean; // is it selected
163+
style?: {...}; // how it looks
164+
geometry?: {...}; // padding settings
165+
}
166+
```
167+
168+
## Advanced Usage
169+
170+
### Custom Group with Extended Properties
171+
172+
You can create your own group type with additional properties. The key is to pass your custom group component to `BlockGroups`. Here's a complete example:
173+
174+
```typescript
175+
// 1. Define extended group type
176+
interface ExtendedTGroup extends TGroup {
177+
description: string;
178+
priority: number;
179+
}
180+
181+
// 2. Create custom group class
182+
class CustomGroup extends Group<ExtendedTGroup> {
183+
protected override render() {
184+
super.render();
185+
const ctx = this.context.ctx;
186+
const rect = this.getRect();
187+
188+
// Draw description
189+
if (this.state.description) {
190+
ctx.font = "12px Arial";
191+
ctx.fillStyle = this.style.textColor;
192+
ctx.fillText(this.state.description, rect.x + 10, rect.y + 25);
193+
}
194+
195+
// Draw priority in top right corner
196+
if (this.state.priority) {
197+
ctx.font = "bold 14px Arial";
198+
const text = `P${this.state.priority}`;
199+
const textWidth = ctx.measureText(text).width;
200+
ctx.fillText(text, rect.x + rect.width - textWidth - 10, rect.y + 25);
201+
}
202+
}
203+
}
204+
205+
// 3. Create groups with extended properties
206+
const blockGroups = graph.addLayer(BlockGroups, {
207+
draggable: false,
208+
groupComponent: CustomGroup, // Use our custom component
209+
});
210+
211+
// 4. Set groups with extended properties
212+
blockGroups.setGroups([
213+
{
214+
id: "group1",
215+
description: "Contains critical blocks",
216+
priority: 1,
217+
rect: { x: 0, y: 0, width: 800, height: 400 }
218+
},
219+
{
220+
id: "group2",
221+
description: "Contains regular blocks",
222+
priority: 2,
223+
rect: { x: 850, y: 0, width: 800, height: 400 }
224+
}
225+
]);
226+
```
227+
228+
## Examples
229+
230+
You can find complete working examples in our Storybook stories:
231+
232+
1. **Basic Groups** ([`default.stories.tsx`](https://github.com/aschetinin/graph/blob/main/src/stories/canvas/groups/default.stories.tsx))
233+
- Shows basic usage of automatic groups
234+
- Groups are created based on block properties
235+
- Demonstrates group styling and behavior
236+
237+
2. **Large Graph** ([`large.stories.tsx`](https://github.com/aschetinin/graph/blob/main/src/stories/canvas/groups/large.stories.tsx))
238+
- Shows how to work with many blocks (225+)
239+
- Automatically groups every 10 blocks
240+
- Demonstrates performance with multiple groups
241+
242+
3. **Manual Groups** ([`manual.stories.tsx`](https://github.com/aschetinin/graph/blob/main/src/stories/canvas/groups/manual.stories.tsx))
243+
- Shows how to create fixed area groups
244+
- Creates non-draggable zones with different colors
245+
- Demonstrates manual group management
246+
247+
4. **Extended Groups** ([`extended.stories.tsx`](https://github.com/aschetinin/graph/blob/main/src/stories/canvas/groups/extended.stories.tsx))
248+
- Shows how to create custom groups with extended properties
249+
- Adds description and priority to groups
250+
- Demonstrates custom rendering and styling
251+
252+
Each example includes different styling approaches and group configurations that you can use as a reference for your own implementation.

src/api/PublicGraphApi.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,7 @@ export class PublicGraphApi {
157157
public unsetSelection() {
158158
batch(() => {
159159
this.graph.rootStore.blocksList.resetSelection();
160+
this.graph.rootStore.groupsList.resetSelection();
160161
this.graph.rootStore.connectionsList.resetSelection();
161162
});
162163
}

src/components/canvas/EventedComponent/EventedComponent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export class EventedComponent<
99
State extends TComponentState = TComponentState,
1010
Context extends TComponentContext = TComponentContext,
1111
> extends Component<Props, State, Context> {
12+
public readonly evented: boolean = true;
13+
1214
public readonly cursor?: string;
1315

1416
private get events() {

src/components/canvas/GraphComponent/index.tsx

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import { Graph } from "../../../graph";
44
import { Component } from "../../../lib";
55
import { TComponentContext, TComponentProps, TComponentState } from "../../../lib/Component";
66
import { HitBox, HitBoxData } from "../../../services/HitTest";
7+
import { getXY } from "../../../utils/functions";
8+
import { dragListener } from "../../../utils/functions/dragListener";
9+
import { EVENTS } from "../../../utils/types/events";
710
import { EventedComponent } from "../EventedComponent/EventedComponent";
811
import { TGraphLayerContext } from "../layers/graphLayer/GraphLayer";
912

@@ -26,6 +29,53 @@ export class GraphComponent<
2629
this.hitBox = new HitBox(this, this.context.graph.hitTest);
2730
}
2831

32+
protected onDrag({
33+
onDragStart,
34+
onDragUpdate,
35+
onDrop,
36+
}: {
37+
onDragStart?: (_event: MouseEvent) => void | boolean;
38+
onDragUpdate?: (
39+
diff: {
40+
prevCoords: [number, number];
41+
currentCoords: [number, number];
42+
diffX: number;
43+
diffY: number;
44+
},
45+
_event: MouseEvent
46+
) => void;
47+
onDrop?: (_event: MouseEvent) => void;
48+
}) {
49+
let startDragCoords: [number, number];
50+
return this.addEventListener("mousedown", (event: MouseEvent) => {
51+
event.stopPropagation();
52+
dragListener(this.context.ownerDocument)
53+
.on(EVENTS.DRAG_START, (event: MouseEvent) => {
54+
if (onDragStart?.(event) === false) {
55+
return;
56+
}
57+
const xy = getXY(this.context.canvas, event);
58+
startDragCoords = this.context.camera.applyToPoint(xy[0], xy[1]);
59+
})
60+
.on(EVENTS.DRAG_UPDATE, (event: MouseEvent) => {
61+
if (!startDragCoords.length) return;
62+
63+
const [canvasX, canvasY] = getXY(this.context.canvas, event);
64+
const currentCoords = this.context.camera.applyToPoint(canvasX, canvasY);
65+
66+
const diffX = (startDragCoords[0] - currentCoords[0]) | 0;
67+
const diffY = (startDragCoords[1] - currentCoords[1]) | 0;
68+
69+
onDragUpdate?.({ prevCoords: startDragCoords, currentCoords, diffX, diffY }, event);
70+
startDragCoords = currentCoords;
71+
})
72+
.on(EVENTS.DRAG_END, (_event: MouseEvent) => {
73+
startDragCoords = undefined;
74+
onDrop?.(_event);
75+
});
76+
});
77+
}
78+
2979
protected subscribeSignal<T>(signal: Signal<T>, cb: (v: T) => void) {
3080
this.unsubscribe.push(signal.subscribe(cb));
3181
}

src/components/canvas/blocks/Block.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export type TBlock<T extends Record<string, unknown> = {}> = {
3232
is: string;
3333
x: number;
3434
y: number;
35+
group?: string;
3536
width: number;
3637
height: number;
3738
selected: boolean;
@@ -227,8 +228,10 @@ export class Block<T extends TBlock = TBlock, Props extends TBlockProps = TBlock
227228
return this.renderOrder;
228229
}
229230

230-
public updatePosition(x: number, y: number) {
231-
this.connectedState.updateXY(x, y);
231+
public updatePosition(x: number, y: number, silent = false) {
232+
if (!silent) {
233+
this.connectedState.updateXY(x, y);
234+
}
232235
this.setState({ x, y });
233236
}
234237

0 commit comments

Comments
 (0)