@formicoidea/labre-framework-bpmn 0.23.0 → 0.23.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/consts.ts +57 -57
- package/src/effects.ts +14 -14
- package/src/element-renderer.ts +93 -93
- package/src/element-view.ts +116 -116
- package/src/index.ts +1 -1
- package/src/node/node-renderer.ts +65 -65
- package/src/node/node-view.ts +34 -34
- package/src/toolbar/bpmn-menu.ts +224 -224
- package/src/toolbar/bpmn-senior-button.ts +102 -102
- package/src/toolbar/config.ts +74 -74
- package/src/toolbar/icons.ts +43 -43
- package/src/toolbar/senior-tool.ts +11 -11
|
@@ -1,65 +1,65 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type ElementRenderer,
|
|
3
|
-
ElementRendererExtension,
|
|
4
|
-
} from '@formicoidea/labre-core/blocks/surface';
|
|
5
|
-
import { shape as shapeRenderer } from '@formicoidea/labre-core/gfx/shape';
|
|
6
|
-
import { type BpmnNodeElementModel, DefaultTheme } from '@formicoidea/labre-core/model';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* Renderer for a BPMN flow-object node. The shape body (ellipse / rounded rect
|
|
10
|
-
* / diamond) is drawn by REUSING the native shape renderer — so stroke width,
|
|
11
|
-
* colors, inner text and theme behave exactly like a native shape. Only
|
|
12
|
-
* `gatewayExclusive` is decorated: an X drawn on top in the node's (editable)
|
|
13
|
-
* stroke color. Events and task are plain native shapes.
|
|
14
|
-
*
|
|
15
|
-
* Mirrors the EDGY node renderer.
|
|
16
|
-
*/
|
|
17
|
-
export const bpmnNode: ElementRenderer<BpmnNodeElementModel> = (
|
|
18
|
-
model,
|
|
19
|
-
ctx,
|
|
20
|
-
matrix,
|
|
21
|
-
renderer,
|
|
22
|
-
rc,
|
|
23
|
-
bound
|
|
24
|
-
) => {
|
|
25
|
-
const [, , w, h] = model.deserializedXYWH;
|
|
26
|
-
const cx = w / 2;
|
|
27
|
-
const cy = h / 2;
|
|
28
|
-
|
|
29
|
-
// Capture the element-local transform BEFORE the shape renderer mutates the
|
|
30
|
-
// matrix, so the glyph can be drawn in the same space afterwards.
|
|
31
|
-
const glyphMatrix = DOMMatrix.fromMatrix(matrix)
|
|
32
|
-
.translateSelf(cx, cy)
|
|
33
|
-
.rotateSelf(model.rotate)
|
|
34
|
-
.translateSelf(-cx, -cy);
|
|
35
|
-
|
|
36
|
-
// Native shape (fill / stroke / inner text / theme handled natively).
|
|
37
|
-
shapeRenderer(model, ctx, matrix, renderer, rc, bound);
|
|
38
|
-
|
|
39
|
-
if (model.kind !== 'gatewayExclusive') return;
|
|
40
|
-
|
|
41
|
-
const color = renderer.getColorValue(
|
|
42
|
-
model.strokeColor,
|
|
43
|
-
DefaultTheme.shapeStrokeColor,
|
|
44
|
-
true
|
|
45
|
-
);
|
|
46
|
-
|
|
47
|
-
// ── Exclusive-gateway X, centred and sized to the diamond ───────────
|
|
48
|
-
const r = Math.min(w, h) * 0.2;
|
|
49
|
-
ctx.setTransform(glyphMatrix);
|
|
50
|
-
ctx.translate(cx, cy);
|
|
51
|
-
ctx.strokeStyle = color;
|
|
52
|
-
ctx.lineWidth = Math.max(2, Math.min(w, h) * 0.06);
|
|
53
|
-
ctx.lineCap = 'round';
|
|
54
|
-
ctx.beginPath();
|
|
55
|
-
ctx.moveTo(-r, -r);
|
|
56
|
-
ctx.lineTo(r, r);
|
|
57
|
-
ctx.moveTo(-r, r);
|
|
58
|
-
ctx.lineTo(r, -r);
|
|
59
|
-
ctx.stroke();
|
|
60
|
-
};
|
|
61
|
-
|
|
62
|
-
export const BpmnNodeRendererExtension = ElementRendererExtension(
|
|
63
|
-
'bpmnNode',
|
|
64
|
-
bpmnNode
|
|
65
|
-
);
|
|
1
|
+
import {
|
|
2
|
+
type ElementRenderer,
|
|
3
|
+
ElementRendererExtension,
|
|
4
|
+
} from '@formicoidea/labre-core/blocks/surface';
|
|
5
|
+
import { shape as shapeRenderer } from '@formicoidea/labre-core/gfx/shape';
|
|
6
|
+
import { type BpmnNodeElementModel, DefaultTheme } from '@formicoidea/labre-core/model';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Renderer for a BPMN flow-object node. The shape body (ellipse / rounded rect
|
|
10
|
+
* / diamond) is drawn by REUSING the native shape renderer — so stroke width,
|
|
11
|
+
* colors, inner text and theme behave exactly like a native shape. Only
|
|
12
|
+
* `gatewayExclusive` is decorated: an X drawn on top in the node's (editable)
|
|
13
|
+
* stroke color. Events and task are plain native shapes.
|
|
14
|
+
*
|
|
15
|
+
* Mirrors the EDGY node renderer.
|
|
16
|
+
*/
|
|
17
|
+
export const bpmnNode: ElementRenderer<BpmnNodeElementModel> = (
|
|
18
|
+
model,
|
|
19
|
+
ctx,
|
|
20
|
+
matrix,
|
|
21
|
+
renderer,
|
|
22
|
+
rc,
|
|
23
|
+
bound
|
|
24
|
+
) => {
|
|
25
|
+
const [, , w, h] = model.deserializedXYWH;
|
|
26
|
+
const cx = w / 2;
|
|
27
|
+
const cy = h / 2;
|
|
28
|
+
|
|
29
|
+
// Capture the element-local transform BEFORE the shape renderer mutates the
|
|
30
|
+
// matrix, so the glyph can be drawn in the same space afterwards.
|
|
31
|
+
const glyphMatrix = DOMMatrix.fromMatrix(matrix)
|
|
32
|
+
.translateSelf(cx, cy)
|
|
33
|
+
.rotateSelf(model.rotate)
|
|
34
|
+
.translateSelf(-cx, -cy);
|
|
35
|
+
|
|
36
|
+
// Native shape (fill / stroke / inner text / theme handled natively).
|
|
37
|
+
shapeRenderer(model, ctx, matrix, renderer, rc, bound);
|
|
38
|
+
|
|
39
|
+
if (model.kind !== 'gatewayExclusive') return;
|
|
40
|
+
|
|
41
|
+
const color = renderer.getColorValue(
|
|
42
|
+
model.strokeColor,
|
|
43
|
+
DefaultTheme.shapeStrokeColor,
|
|
44
|
+
true
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
// ── Exclusive-gateway X, centred and sized to the diamond ───────────
|
|
48
|
+
const r = Math.min(w, h) * 0.2;
|
|
49
|
+
ctx.setTransform(glyphMatrix);
|
|
50
|
+
ctx.translate(cx, cy);
|
|
51
|
+
ctx.strokeStyle = color;
|
|
52
|
+
ctx.lineWidth = Math.max(2, Math.min(w, h) * 0.06);
|
|
53
|
+
ctx.lineCap = 'round';
|
|
54
|
+
ctx.beginPath();
|
|
55
|
+
ctx.moveTo(-r, -r);
|
|
56
|
+
ctx.lineTo(r, r);
|
|
57
|
+
ctx.moveTo(-r, r);
|
|
58
|
+
ctx.lineTo(r, -r);
|
|
59
|
+
ctx.stroke();
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const BpmnNodeRendererExtension = ElementRendererExtension(
|
|
63
|
+
'bpmnNode',
|
|
64
|
+
bpmnNode
|
|
65
|
+
);
|
package/src/node/node-view.ts
CHANGED
|
@@ -1,34 +1,34 @@
|
|
|
1
|
-
import { mountShapeTextEditor } from '@formicoidea/labre-core/gfx/shape';
|
|
2
|
-
import {
|
|
3
|
-
type BpmnNodeElementModel,
|
|
4
|
-
ShapeElementModel,
|
|
5
|
-
} from '@formicoidea/labre-core/model';
|
|
6
|
-
import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* View for a BPMN flow-object node. Registering it ensures `gfx.view.get(model)`
|
|
10
|
-
* returns a view (required so move / select / connector interactions work).
|
|
11
|
-
*
|
|
12
|
-
* BPMN nodes are native shapes, so we reuse the shape inner-text editor: a
|
|
13
|
-
* double-click mounts the editable text overlay (`mountShapeTextEditor`),
|
|
14
|
-
* exactly like a native shape (used to label the task).
|
|
15
|
-
*
|
|
16
|
-
* Mirrors {@link EdgyNodeView}.
|
|
17
|
-
*/
|
|
18
|
-
export class BpmnNodeView extends GfxElementModelView<BpmnNodeElementModel> {
|
|
19
|
-
static override type: string = 'bpmnNode';
|
|
20
|
-
|
|
21
|
-
override onCreated(): void {
|
|
22
|
-
super.onCreated();
|
|
23
|
-
this.on('dblclick', () => {
|
|
24
|
-
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
|
|
25
|
-
if (
|
|
26
|
-
edgeless &&
|
|
27
|
-
!this.model.isLocked() &&
|
|
28
|
-
this.model instanceof ShapeElementModel
|
|
29
|
-
) {
|
|
30
|
-
mountShapeTextEditor(this.model, edgeless);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
}
|
|
34
|
-
}
|
|
1
|
+
import { mountShapeTextEditor } from '@formicoidea/labre-core/gfx/shape';
|
|
2
|
+
import {
|
|
3
|
+
type BpmnNodeElementModel,
|
|
4
|
+
ShapeElementModel,
|
|
5
|
+
} from '@formicoidea/labre-core/model';
|
|
6
|
+
import { GfxElementModelView } from '@formicoidea/labre-core/std/gfx';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* View for a BPMN flow-object node. Registering it ensures `gfx.view.get(model)`
|
|
10
|
+
* returns a view (required so move / select / connector interactions work).
|
|
11
|
+
*
|
|
12
|
+
* BPMN nodes are native shapes, so we reuse the shape inner-text editor: a
|
|
13
|
+
* double-click mounts the editable text overlay (`mountShapeTextEditor`),
|
|
14
|
+
* exactly like a native shape (used to label the task).
|
|
15
|
+
*
|
|
16
|
+
* Mirrors {@link EdgyNodeView}.
|
|
17
|
+
*/
|
|
18
|
+
export class BpmnNodeView extends GfxElementModelView<BpmnNodeElementModel> {
|
|
19
|
+
static override type: string = 'bpmnNode';
|
|
20
|
+
|
|
21
|
+
override onCreated(): void {
|
|
22
|
+
super.onCreated();
|
|
23
|
+
this.on('dblclick', () => {
|
|
24
|
+
const edgeless = this.std.view.getBlock(this.std.store.root!.id);
|
|
25
|
+
if (
|
|
26
|
+
edgeless &&
|
|
27
|
+
!this.model.isLocked() &&
|
|
28
|
+
this.model instanceof ShapeElementModel
|
|
29
|
+
) {
|
|
30
|
+
mountShapeTextEditor(this.model, edgeless);
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
package/src/toolbar/bpmn-menu.ts
CHANGED
|
@@ -1,224 +1,224 @@
|
|
|
1
|
-
import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
-
import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
|
|
3
|
-
import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
|
|
4
|
-
import {
|
|
5
|
-
ConnectorMode,
|
|
6
|
-
FontFamily,
|
|
7
|
-
PointStyle,
|
|
8
|
-
ShapeStyle,
|
|
9
|
-
StrokeStyle,
|
|
10
|
-
} from '@formicoidea/labre-core/model';
|
|
11
|
-
import {
|
|
12
|
-
EditPropsStore,
|
|
13
|
-
TelemetryProvider,
|
|
14
|
-
} from '@formicoidea/labre-core/shared/services';
|
|
15
|
-
import { EdgelessToolbarToolMixin } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
|
|
16
|
-
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
17
|
-
import { css, html, LitElement } from 'lit';
|
|
18
|
-
|
|
19
|
-
import {
|
|
20
|
-
EVENT_END,
|
|
21
|
-
EVENT_START,
|
|
22
|
-
END_WIDTH,
|
|
23
|
-
INNER_FONT_SIZE,
|
|
24
|
-
NEUTRAL_STROKE,
|
|
25
|
-
NODE_FILL,
|
|
26
|
-
NODE_LABEL,
|
|
27
|
-
NODE_SIZE,
|
|
28
|
-
NODE_STROKE_WIDTH,
|
|
29
|
-
SEQUENCE_STROKE,
|
|
30
|
-
SEQUENCE_WIDTH,
|
|
31
|
-
START_WIDTH,
|
|
32
|
-
TASK_RADIUS,
|
|
33
|
-
} from '../consts';
|
|
34
|
-
import {
|
|
35
|
-
bpmnEndIcon,
|
|
36
|
-
bpmnGatewayIcon,
|
|
37
|
-
bpmnPoolIcon,
|
|
38
|
-
bpmnSequenceIcon,
|
|
39
|
-
bpmnStartIcon,
|
|
40
|
-
bpmnTaskIcon,
|
|
41
|
-
} from './icons';
|
|
42
|
-
|
|
43
|
-
type NodeKind = 'startEvent' | 'endEvent' | 'task' | 'gatewayExclusive';
|
|
44
|
-
|
|
45
|
-
/** Per-kind native shape + accent presets (style C). */
|
|
46
|
-
const NODE_PRESETS: Record<
|
|
47
|
-
NodeKind,
|
|
48
|
-
{ shapeType: 'ellipse' | 'rect' | 'diamond'; stroke: string; width: number }
|
|
49
|
-
> = {
|
|
50
|
-
startEvent: { shapeType: 'ellipse', stroke: EVENT_START, width: START_WIDTH },
|
|
51
|
-
endEvent: { shapeType: 'ellipse', stroke: EVENT_END, width: END_WIDTH },
|
|
52
|
-
task: { shapeType: 'rect', stroke: NEUTRAL_STROKE, width: NODE_STROKE_WIDTH },
|
|
53
|
-
gatewayExclusive: {
|
|
54
|
-
shapeType: 'diamond',
|
|
55
|
-
stroke: NEUTRAL_STROKE,
|
|
56
|
-
width: NODE_STROKE_WIDTH,
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* The popover above the toolbar for the BPMN toolbox. Items create the flow
|
|
62
|
-
* objects (native shapes, so they stay editable), the pool background, and arm
|
|
63
|
-
* the native connector tool for sequence flows. Mirrors the EDGY menu.
|
|
64
|
-
*/
|
|
65
|
-
export class EdgelessBpmnMenu extends EdgelessToolbarToolMixin(LitElement) {
|
|
66
|
-
static override styles = css`
|
|
67
|
-
:host {
|
|
68
|
-
position: absolute;
|
|
69
|
-
display: flex;
|
|
70
|
-
z-index: -1;
|
|
71
|
-
}
|
|
72
|
-
.menu-content {
|
|
73
|
-
display: flex;
|
|
74
|
-
align-items: center;
|
|
75
|
-
justify-content: center;
|
|
76
|
-
}
|
|
77
|
-
.button-group-container {
|
|
78
|
-
display: flex;
|
|
79
|
-
align-items: center;
|
|
80
|
-
gap: 14px;
|
|
81
|
-
fill: var(--affine-icon-color);
|
|
82
|
-
}
|
|
83
|
-
.button-group-container svg {
|
|
84
|
-
width: 24px;
|
|
85
|
-
height: 24px;
|
|
86
|
-
}
|
|
87
|
-
`;
|
|
88
|
-
|
|
89
|
-
override type = EmptyTool;
|
|
90
|
-
|
|
91
|
-
/** Create a flow-object node (native shape) centred on the viewport. */
|
|
92
|
-
private _createNode(kind: NodeKind) {
|
|
93
|
-
const surface = this.gfx.surface;
|
|
94
|
-
if (!surface) return;
|
|
95
|
-
|
|
96
|
-
const { w, h } = NODE_SIZE[kind];
|
|
97
|
-
const { centerX: cx, centerY: cy } = this.gfx.viewport;
|
|
98
|
-
const preset = NODE_PRESETS[kind];
|
|
99
|
-
|
|
100
|
-
const id = surface.addElement({
|
|
101
|
-
type: 'bpmnNode',
|
|
102
|
-
kind,
|
|
103
|
-
shapeType: preset.shapeType,
|
|
104
|
-
filled: true,
|
|
105
|
-
fillColor: NODE_FILL,
|
|
106
|
-
strokeColor: preset.stroke,
|
|
107
|
-
strokeWidth: preset.width,
|
|
108
|
-
shapeStyle: ShapeStyle.General,
|
|
109
|
-
roughness: 0,
|
|
110
|
-
radius: kind === 'task' ? TASK_RADIUS : 0,
|
|
111
|
-
text: NODE_LABEL[kind] || undefined,
|
|
112
|
-
color: NEUTRAL_STROKE,
|
|
113
|
-
fontFamily: FontFamily.Inter,
|
|
114
|
-
fontSize: INNER_FONT_SIZE,
|
|
115
|
-
textAlign: 'center',
|
|
116
|
-
xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
|
|
117
|
-
});
|
|
118
|
-
this._track('FrameworkElementAdded', `node:${kind}`);
|
|
119
|
-
this._finish(id);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/** Create a pool (background container) centred on the viewport. */
|
|
123
|
-
private _createPool() {
|
|
124
|
-
const surface = this.gfx.surface;
|
|
125
|
-
if (!surface) return;
|
|
126
|
-
|
|
127
|
-
const w = 560;
|
|
128
|
-
const h = 200;
|
|
129
|
-
const { centerX: cx, centerY: cy } = this.gfx.viewport;
|
|
130
|
-
const id = surface.addElement({
|
|
131
|
-
type: 'bpmnPool',
|
|
132
|
-
xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
|
|
133
|
-
});
|
|
134
|
-
this._track('FrameworkElementAdded', 'pool');
|
|
135
|
-
this._finish(id);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Arm the native connector tool, pre-styled for a BPMN sequence flow:
|
|
140
|
-
* orthogonal, solid, with a filled triangle head. The user then draws from
|
|
141
|
-
* one node to another (endpoints attach to centers).
|
|
142
|
-
*/
|
|
143
|
-
private _activateSequenceFlow() {
|
|
144
|
-
this.edgeless.std.get(EditPropsStore).recordLastProps('connector', {
|
|
145
|
-
mode: ConnectorMode.Orthogonal,
|
|
146
|
-
stroke: SEQUENCE_STROKE,
|
|
147
|
-
strokeStyle: StrokeStyle.Solid,
|
|
148
|
-
strokeWidth: SEQUENCE_WIDTH,
|
|
149
|
-
frontEndpointStyle: PointStyle.None,
|
|
150
|
-
rearEndpointStyle: PointStyle.Triangle,
|
|
151
|
-
});
|
|
152
|
-
this._track('FrameworkToolPicked', 'connector:sequence');
|
|
153
|
-
this.gfx.tool.setTool(ConnectorTool, { mode: ConnectorMode.Orthogonal });
|
|
154
|
-
// Keep the palette open (native sub-menu behaviour).
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
private _finish(id: string) {
|
|
158
|
-
const { gfx } = this;
|
|
159
|
-
gfx.doc.captureSync();
|
|
160
|
-
gfx.tool.setTool(DefaultTool);
|
|
161
|
-
gfx.selection.set({ elements: [id], editing: false });
|
|
162
|
-
// Keep the palette open (native sub-menu behaviour).
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
private _track(
|
|
166
|
-
event: 'FrameworkElementAdded' | 'FrameworkToolPicked',
|
|
167
|
-
element: string
|
|
168
|
-
) {
|
|
169
|
-
this.edgeless.std.getOptional(TelemetryProvider)?.track(event, {
|
|
170
|
-
framework: 'bpmn',
|
|
171
|
-
element,
|
|
172
|
-
page: 'whiteboard editor',
|
|
173
|
-
segment: 'bpmn toolbox',
|
|
174
|
-
module: 'bpmn menu',
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
override render() {
|
|
179
|
-
return html`
|
|
180
|
-
<edgeless-slide-menu>
|
|
181
|
-
<div class="menu-content">
|
|
182
|
-
<div class="button-group-container">
|
|
183
|
-
<edgeless-tool-icon-button
|
|
184
|
-
.tooltip=${'Start event'}
|
|
185
|
-
@click=${() => this._createNode('startEvent')}
|
|
186
|
-
>
|
|
187
|
-
${bpmnStartIcon}
|
|
188
|
-
</edgeless-tool-icon-button>
|
|
189
|
-
<edgeless-tool-icon-button
|
|
190
|
-
.tooltip=${'End event'}
|
|
191
|
-
@click=${() => this._createNode('endEvent')}
|
|
192
|
-
>
|
|
193
|
-
${bpmnEndIcon}
|
|
194
|
-
</edgeless-tool-icon-button>
|
|
195
|
-
<edgeless-tool-icon-button
|
|
196
|
-
.tooltip=${'Task'}
|
|
197
|
-
@click=${() => this._createNode('task')}
|
|
198
|
-
>
|
|
199
|
-
${bpmnTaskIcon}
|
|
200
|
-
</edgeless-tool-icon-button>
|
|
201
|
-
<edgeless-tool-icon-button
|
|
202
|
-
.tooltip=${'Exclusive gateway'}
|
|
203
|
-
@click=${() => this._createNode('gatewayExclusive')}
|
|
204
|
-
>
|
|
205
|
-
${bpmnGatewayIcon}
|
|
206
|
-
</edgeless-tool-icon-button>
|
|
207
|
-
<edgeless-tool-icon-button
|
|
208
|
-
.tooltip=${'Sequence flow'}
|
|
209
|
-
@click=${this._activateSequenceFlow}
|
|
210
|
-
>
|
|
211
|
-
${bpmnSequenceIcon}
|
|
212
|
-
</edgeless-tool-icon-button>
|
|
213
|
-
<edgeless-tool-icon-button
|
|
214
|
-
.tooltip=${'Pool'}
|
|
215
|
-
@click=${this._createPool}
|
|
216
|
-
>
|
|
217
|
-
${bpmnPoolIcon}
|
|
218
|
-
</edgeless-tool-icon-button>
|
|
219
|
-
</div>
|
|
220
|
-
</div>
|
|
221
|
-
</edgeless-slide-menu>
|
|
222
|
-
`;
|
|
223
|
-
}
|
|
224
|
-
}
|
|
1
|
+
import { DefaultTool } from '@formicoidea/labre-core/blocks/surface';
|
|
2
|
+
import { ConnectorTool } from '@formicoidea/labre-core/gfx/connector';
|
|
3
|
+
import { EmptyTool } from '@formicoidea/labre-core/gfx/pointer';
|
|
4
|
+
import {
|
|
5
|
+
ConnectorMode,
|
|
6
|
+
FontFamily,
|
|
7
|
+
PointStyle,
|
|
8
|
+
ShapeStyle,
|
|
9
|
+
StrokeStyle,
|
|
10
|
+
} from '@formicoidea/labre-core/model';
|
|
11
|
+
import {
|
|
12
|
+
EditPropsStore,
|
|
13
|
+
TelemetryProvider,
|
|
14
|
+
} from '@formicoidea/labre-core/shared/services';
|
|
15
|
+
import { EdgelessToolbarToolMixin } from '@formicoidea/labre-core/widgets/edgeless-toolbar';
|
|
16
|
+
import { Bound } from '@formicoidea/labre-core/global/gfx';
|
|
17
|
+
import { css, html, LitElement } from 'lit';
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
EVENT_END,
|
|
21
|
+
EVENT_START,
|
|
22
|
+
END_WIDTH,
|
|
23
|
+
INNER_FONT_SIZE,
|
|
24
|
+
NEUTRAL_STROKE,
|
|
25
|
+
NODE_FILL,
|
|
26
|
+
NODE_LABEL,
|
|
27
|
+
NODE_SIZE,
|
|
28
|
+
NODE_STROKE_WIDTH,
|
|
29
|
+
SEQUENCE_STROKE,
|
|
30
|
+
SEQUENCE_WIDTH,
|
|
31
|
+
START_WIDTH,
|
|
32
|
+
TASK_RADIUS,
|
|
33
|
+
} from '../consts';
|
|
34
|
+
import {
|
|
35
|
+
bpmnEndIcon,
|
|
36
|
+
bpmnGatewayIcon,
|
|
37
|
+
bpmnPoolIcon,
|
|
38
|
+
bpmnSequenceIcon,
|
|
39
|
+
bpmnStartIcon,
|
|
40
|
+
bpmnTaskIcon,
|
|
41
|
+
} from './icons';
|
|
42
|
+
|
|
43
|
+
type NodeKind = 'startEvent' | 'endEvent' | 'task' | 'gatewayExclusive';
|
|
44
|
+
|
|
45
|
+
/** Per-kind native shape + accent presets (style C). */
|
|
46
|
+
const NODE_PRESETS: Record<
|
|
47
|
+
NodeKind,
|
|
48
|
+
{ shapeType: 'ellipse' | 'rect' | 'diamond'; stroke: string; width: number }
|
|
49
|
+
> = {
|
|
50
|
+
startEvent: { shapeType: 'ellipse', stroke: EVENT_START, width: START_WIDTH },
|
|
51
|
+
endEvent: { shapeType: 'ellipse', stroke: EVENT_END, width: END_WIDTH },
|
|
52
|
+
task: { shapeType: 'rect', stroke: NEUTRAL_STROKE, width: NODE_STROKE_WIDTH },
|
|
53
|
+
gatewayExclusive: {
|
|
54
|
+
shapeType: 'diamond',
|
|
55
|
+
stroke: NEUTRAL_STROKE,
|
|
56
|
+
width: NODE_STROKE_WIDTH,
|
|
57
|
+
},
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* The popover above the toolbar for the BPMN toolbox. Items create the flow
|
|
62
|
+
* objects (native shapes, so they stay editable), the pool background, and arm
|
|
63
|
+
* the native connector tool for sequence flows. Mirrors the EDGY menu.
|
|
64
|
+
*/
|
|
65
|
+
export class EdgelessBpmnMenu extends EdgelessToolbarToolMixin(LitElement) {
|
|
66
|
+
static override styles = css`
|
|
67
|
+
:host {
|
|
68
|
+
position: absolute;
|
|
69
|
+
display: flex;
|
|
70
|
+
z-index: -1;
|
|
71
|
+
}
|
|
72
|
+
.menu-content {
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
justify-content: center;
|
|
76
|
+
}
|
|
77
|
+
.button-group-container {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: 14px;
|
|
81
|
+
fill: var(--affine-icon-color);
|
|
82
|
+
}
|
|
83
|
+
.button-group-container svg {
|
|
84
|
+
width: 24px;
|
|
85
|
+
height: 24px;
|
|
86
|
+
}
|
|
87
|
+
`;
|
|
88
|
+
|
|
89
|
+
override type = EmptyTool;
|
|
90
|
+
|
|
91
|
+
/** Create a flow-object node (native shape) centred on the viewport. */
|
|
92
|
+
private _createNode(kind: NodeKind) {
|
|
93
|
+
const surface = this.gfx.surface;
|
|
94
|
+
if (!surface) return;
|
|
95
|
+
|
|
96
|
+
const { w, h } = NODE_SIZE[kind];
|
|
97
|
+
const { centerX: cx, centerY: cy } = this.gfx.viewport;
|
|
98
|
+
const preset = NODE_PRESETS[kind];
|
|
99
|
+
|
|
100
|
+
const id = surface.addElement({
|
|
101
|
+
type: 'bpmnNode',
|
|
102
|
+
kind,
|
|
103
|
+
shapeType: preset.shapeType,
|
|
104
|
+
filled: true,
|
|
105
|
+
fillColor: NODE_FILL,
|
|
106
|
+
strokeColor: preset.stroke,
|
|
107
|
+
strokeWidth: preset.width,
|
|
108
|
+
shapeStyle: ShapeStyle.General,
|
|
109
|
+
roughness: 0,
|
|
110
|
+
radius: kind === 'task' ? TASK_RADIUS : 0,
|
|
111
|
+
text: NODE_LABEL[kind] || undefined,
|
|
112
|
+
color: NEUTRAL_STROKE,
|
|
113
|
+
fontFamily: FontFamily.Inter,
|
|
114
|
+
fontSize: INNER_FONT_SIZE,
|
|
115
|
+
textAlign: 'center',
|
|
116
|
+
xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
|
|
117
|
+
});
|
|
118
|
+
this._track('FrameworkElementAdded', `node:${kind}`);
|
|
119
|
+
this._finish(id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Create a pool (background container) centred on the viewport. */
|
|
123
|
+
private _createPool() {
|
|
124
|
+
const surface = this.gfx.surface;
|
|
125
|
+
if (!surface) return;
|
|
126
|
+
|
|
127
|
+
const w = 560;
|
|
128
|
+
const h = 200;
|
|
129
|
+
const { centerX: cx, centerY: cy } = this.gfx.viewport;
|
|
130
|
+
const id = surface.addElement({
|
|
131
|
+
type: 'bpmnPool',
|
|
132
|
+
xywh: new Bound(cx - w / 2, cy - h / 2, w, h).serialize(),
|
|
133
|
+
});
|
|
134
|
+
this._track('FrameworkElementAdded', 'pool');
|
|
135
|
+
this._finish(id);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Arm the native connector tool, pre-styled for a BPMN sequence flow:
|
|
140
|
+
* orthogonal, solid, with a filled triangle head. The user then draws from
|
|
141
|
+
* one node to another (endpoints attach to centers).
|
|
142
|
+
*/
|
|
143
|
+
private _activateSequenceFlow() {
|
|
144
|
+
this.edgeless.std.get(EditPropsStore).recordLastProps('connector', {
|
|
145
|
+
mode: ConnectorMode.Orthogonal,
|
|
146
|
+
stroke: SEQUENCE_STROKE,
|
|
147
|
+
strokeStyle: StrokeStyle.Solid,
|
|
148
|
+
strokeWidth: SEQUENCE_WIDTH,
|
|
149
|
+
frontEndpointStyle: PointStyle.None,
|
|
150
|
+
rearEndpointStyle: PointStyle.Triangle,
|
|
151
|
+
});
|
|
152
|
+
this._track('FrameworkToolPicked', 'connector:sequence');
|
|
153
|
+
this.gfx.tool.setTool(ConnectorTool, { mode: ConnectorMode.Orthogonal });
|
|
154
|
+
// Keep the palette open (native sub-menu behaviour).
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private _finish(id: string) {
|
|
158
|
+
const { gfx } = this;
|
|
159
|
+
gfx.doc.captureSync();
|
|
160
|
+
gfx.tool.setTool(DefaultTool);
|
|
161
|
+
gfx.selection.set({ elements: [id], editing: false });
|
|
162
|
+
// Keep the palette open (native sub-menu behaviour).
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private _track(
|
|
166
|
+
event: 'FrameworkElementAdded' | 'FrameworkToolPicked',
|
|
167
|
+
element: string
|
|
168
|
+
) {
|
|
169
|
+
this.edgeless.std.getOptional(TelemetryProvider)?.track(event, {
|
|
170
|
+
framework: 'bpmn',
|
|
171
|
+
element,
|
|
172
|
+
page: 'whiteboard editor',
|
|
173
|
+
segment: 'bpmn toolbox',
|
|
174
|
+
module: 'bpmn menu',
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
override render() {
|
|
179
|
+
return html`
|
|
180
|
+
<edgeless-slide-menu>
|
|
181
|
+
<div class="menu-content">
|
|
182
|
+
<div class="button-group-container">
|
|
183
|
+
<edgeless-tool-icon-button
|
|
184
|
+
.tooltip=${'Start event'}
|
|
185
|
+
@click=${() => this._createNode('startEvent')}
|
|
186
|
+
>
|
|
187
|
+
${bpmnStartIcon}
|
|
188
|
+
</edgeless-tool-icon-button>
|
|
189
|
+
<edgeless-tool-icon-button
|
|
190
|
+
.tooltip=${'End event'}
|
|
191
|
+
@click=${() => this._createNode('endEvent')}
|
|
192
|
+
>
|
|
193
|
+
${bpmnEndIcon}
|
|
194
|
+
</edgeless-tool-icon-button>
|
|
195
|
+
<edgeless-tool-icon-button
|
|
196
|
+
.tooltip=${'Task'}
|
|
197
|
+
@click=${() => this._createNode('task')}
|
|
198
|
+
>
|
|
199
|
+
${bpmnTaskIcon}
|
|
200
|
+
</edgeless-tool-icon-button>
|
|
201
|
+
<edgeless-tool-icon-button
|
|
202
|
+
.tooltip=${'Exclusive gateway'}
|
|
203
|
+
@click=${() => this._createNode('gatewayExclusive')}
|
|
204
|
+
>
|
|
205
|
+
${bpmnGatewayIcon}
|
|
206
|
+
</edgeless-tool-icon-button>
|
|
207
|
+
<edgeless-tool-icon-button
|
|
208
|
+
.tooltip=${'Sequence flow'}
|
|
209
|
+
@click=${this._activateSequenceFlow}
|
|
210
|
+
>
|
|
211
|
+
${bpmnSequenceIcon}
|
|
212
|
+
</edgeless-tool-icon-button>
|
|
213
|
+
<edgeless-tool-icon-button
|
|
214
|
+
.tooltip=${'Pool'}
|
|
215
|
+
@click=${this._createPool}
|
|
216
|
+
>
|
|
217
|
+
${bpmnPoolIcon}
|
|
218
|
+
</edgeless-tool-icon-button>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</edgeless-slide-menu>
|
|
222
|
+
`;
|
|
223
|
+
}
|
|
224
|
+
}
|