pict-section-flow 0.0.1
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/LICENSE +21 -0
- package/example_application/css/flowexample.css +65 -0
- package/example_application/html/index.html +32 -0
- package/example_application/package.json +41 -0
- package/example_application/source/Pict-Application-FlowExample-Configuration.json +15 -0
- package/example_application/source/Pict-Application-FlowExample.js +241 -0
- package/example_application/source/providers/PictRouter-FlowExample-Configuration.json +22 -0
- package/example_application/source/views/PictView-FlowExample-About.js +184 -0
- package/example_application/source/views/PictView-FlowExample-BottomBar.js +77 -0
- package/example_application/source/views/PictView-FlowExample-Documentation.js +325 -0
- package/example_application/source/views/PictView-FlowExample-Layout.js +86 -0
- package/example_application/source/views/PictView-FlowExample-MainWorkspace.js +191 -0
- package/example_application/source/views/PictView-FlowExample-TopBar.js +95 -0
- package/package.json +22 -0
- package/source/Pict-Section-Flow.js +19 -0
- package/source/providers/PictProvider-Flow-EventHandler.js +158 -0
- package/source/providers/PictProvider-Flow-NodeTypes.js +174 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +251 -0
- package/source/services/PictService-Flow-InteractionManager.js +567 -0
- package/source/services/PictService-Flow-Layout.js +207 -0
- package/source/views/PictView-Flow-Node.js +267 -0
- package/source/views/PictView-Flow-Toolbar.js +223 -0
- package/source/views/PictView-Flow.js +1116 -0
|
@@ -0,0 +1,1116 @@
|
|
|
1
|
+
const libPictView = require('pict-view');
|
|
2
|
+
|
|
3
|
+
const libPictServiceFlowInteractionManager = require('../services/PictService-Flow-InteractionManager.js');
|
|
4
|
+
const libPictServiceFlowConnectionRenderer = require('../services/PictService-Flow-ConnectionRenderer.js');
|
|
5
|
+
const libPictServiceFlowLayout = require('../services/PictService-Flow-Layout.js');
|
|
6
|
+
|
|
7
|
+
const libPictProviderFlowNodeTypes = require('../providers/PictProvider-Flow-NodeTypes.js');
|
|
8
|
+
const libPictProviderFlowEventHandler = require('../providers/PictProvider-Flow-EventHandler.js');
|
|
9
|
+
|
|
10
|
+
const libPictViewFlowNode = require('./PictView-Flow-Node.js');
|
|
11
|
+
const libPictViewFlowToolbar = require('./PictView-Flow-Toolbar.js');
|
|
12
|
+
|
|
13
|
+
const _DefaultConfiguration =
|
|
14
|
+
{
|
|
15
|
+
ViewIdentifier: 'Pict-Flow',
|
|
16
|
+
|
|
17
|
+
DefaultRenderable: 'Flow-Container',
|
|
18
|
+
DefaultDestinationAddress: '#Flow-Container',
|
|
19
|
+
|
|
20
|
+
AutoRender: false,
|
|
21
|
+
|
|
22
|
+
FlowDataAddress: false,
|
|
23
|
+
|
|
24
|
+
TargetElementAddress: '#Flow-SVG-Container',
|
|
25
|
+
|
|
26
|
+
EnableToolbar: true,
|
|
27
|
+
EnablePanning: true,
|
|
28
|
+
EnableZooming: true,
|
|
29
|
+
EnableNodeDragging: true,
|
|
30
|
+
EnableConnectionCreation: true,
|
|
31
|
+
EnableGridSnap: false,
|
|
32
|
+
GridSnapSize: 20,
|
|
33
|
+
|
|
34
|
+
MinZoom: 0.1,
|
|
35
|
+
MaxZoom: 5.0,
|
|
36
|
+
ZoomStep: 0.1,
|
|
37
|
+
|
|
38
|
+
DefaultNodeType: 'default',
|
|
39
|
+
DefaultNodeWidth: 180,
|
|
40
|
+
DefaultNodeHeight: 80,
|
|
41
|
+
|
|
42
|
+
CSS: /*css*/`
|
|
43
|
+
.pict-flow-container {
|
|
44
|
+
position: relative;
|
|
45
|
+
width: 100%;
|
|
46
|
+
height: 100%;
|
|
47
|
+
min-height: 400px;
|
|
48
|
+
overflow: hidden;
|
|
49
|
+
background-color: #fafafa;
|
|
50
|
+
border: 1px solid #e0e0e0;
|
|
51
|
+
border-radius: 4px;
|
|
52
|
+
}
|
|
53
|
+
.pict-flow-svg {
|
|
54
|
+
width: 100%;
|
|
55
|
+
height: 100%;
|
|
56
|
+
min-height: 400px;
|
|
57
|
+
cursor: grab;
|
|
58
|
+
user-select: none;
|
|
59
|
+
-webkit-user-select: none;
|
|
60
|
+
}
|
|
61
|
+
.pict-flow-svg.panning {
|
|
62
|
+
cursor: grabbing;
|
|
63
|
+
}
|
|
64
|
+
.pict-flow-svg.connecting {
|
|
65
|
+
cursor: crosshair;
|
|
66
|
+
}
|
|
67
|
+
.pict-flow-grid-pattern line {
|
|
68
|
+
stroke: #e8e8e8;
|
|
69
|
+
stroke-width: 0.5;
|
|
70
|
+
}
|
|
71
|
+
.pict-flow-node {
|
|
72
|
+
cursor: pointer;
|
|
73
|
+
}
|
|
74
|
+
.pict-flow-node:hover .pict-flow-node-body {
|
|
75
|
+
filter: brightness(0.97);
|
|
76
|
+
}
|
|
77
|
+
.pict-flow-node.selected .pict-flow-node-body {
|
|
78
|
+
stroke: #3498db;
|
|
79
|
+
stroke-width: 2.5;
|
|
80
|
+
}
|
|
81
|
+
.pict-flow-node.dragging {
|
|
82
|
+
opacity: 0.85;
|
|
83
|
+
cursor: grabbing;
|
|
84
|
+
}
|
|
85
|
+
.pict-flow-node-body {
|
|
86
|
+
fill: #ffffff;
|
|
87
|
+
stroke: #bdc3c7;
|
|
88
|
+
stroke-width: 1.5;
|
|
89
|
+
rx: 6;
|
|
90
|
+
ry: 6;
|
|
91
|
+
transition: filter 0.15s;
|
|
92
|
+
}
|
|
93
|
+
.pict-flow-node-title-bar {
|
|
94
|
+
fill: #2c3e50;
|
|
95
|
+
rx: 6;
|
|
96
|
+
ry: 6;
|
|
97
|
+
}
|
|
98
|
+
.pict-flow-node-title-bar-bottom {
|
|
99
|
+
fill: #2c3e50;
|
|
100
|
+
}
|
|
101
|
+
.pict-flow-node-title {
|
|
102
|
+
fill: #ffffff;
|
|
103
|
+
font-size: 12px;
|
|
104
|
+
font-weight: 700;
|
|
105
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
106
|
+
pointer-events: none;
|
|
107
|
+
}
|
|
108
|
+
.pict-flow-node-type-label {
|
|
109
|
+
fill: #95a5a6;
|
|
110
|
+
font-size: 10px;
|
|
111
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
112
|
+
pointer-events: none;
|
|
113
|
+
}
|
|
114
|
+
.pict-flow-port {
|
|
115
|
+
cursor: crosshair;
|
|
116
|
+
transition: r 0.15s;
|
|
117
|
+
}
|
|
118
|
+
.pict-flow-port.input {
|
|
119
|
+
fill: #3498db;
|
|
120
|
+
stroke: #2980b9;
|
|
121
|
+
stroke-width: 1.5;
|
|
122
|
+
}
|
|
123
|
+
.pict-flow-port.output {
|
|
124
|
+
fill: #2ecc71;
|
|
125
|
+
stroke: #27ae60;
|
|
126
|
+
stroke-width: 1.5;
|
|
127
|
+
}
|
|
128
|
+
.pict-flow-port:hover {
|
|
129
|
+
r: 7;
|
|
130
|
+
}
|
|
131
|
+
.pict-flow-port-label {
|
|
132
|
+
fill: #7f8c8d;
|
|
133
|
+
font-size: 9px;
|
|
134
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
135
|
+
pointer-events: none;
|
|
136
|
+
}
|
|
137
|
+
.pict-flow-connection {
|
|
138
|
+
fill: none;
|
|
139
|
+
stroke: #95a5a6;
|
|
140
|
+
stroke-width: 2;
|
|
141
|
+
cursor: pointer;
|
|
142
|
+
transition: stroke 0.15s;
|
|
143
|
+
}
|
|
144
|
+
.pict-flow-connection:hover {
|
|
145
|
+
stroke: #7f8c8d;
|
|
146
|
+
stroke-width: 3;
|
|
147
|
+
}
|
|
148
|
+
.pict-flow-connection.selected {
|
|
149
|
+
stroke: #3498db;
|
|
150
|
+
stroke-width: 3;
|
|
151
|
+
}
|
|
152
|
+
.pict-flow-connection-hitarea {
|
|
153
|
+
fill: none;
|
|
154
|
+
stroke: transparent;
|
|
155
|
+
stroke-width: 12;
|
|
156
|
+
cursor: pointer;
|
|
157
|
+
}
|
|
158
|
+
.pict-flow-drag-connection {
|
|
159
|
+
fill: none;
|
|
160
|
+
stroke: #3498db;
|
|
161
|
+
stroke-width: 2;
|
|
162
|
+
stroke-dasharray: 6 3;
|
|
163
|
+
pointer-events: none;
|
|
164
|
+
}
|
|
165
|
+
.pict-flow-node-decision .pict-flow-node-body {
|
|
166
|
+
fill: #fff9e6;
|
|
167
|
+
stroke: #f39c12;
|
|
168
|
+
}
|
|
169
|
+
.pict-flow-node-start .pict-flow-node-body {
|
|
170
|
+
fill: #eafaf1;
|
|
171
|
+
stroke: #27ae60;
|
|
172
|
+
rx: 25;
|
|
173
|
+
ry: 25;
|
|
174
|
+
}
|
|
175
|
+
.pict-flow-node-end .pict-flow-node-body {
|
|
176
|
+
fill: #fdedec;
|
|
177
|
+
stroke: #e74c3c;
|
|
178
|
+
rx: 25;
|
|
179
|
+
ry: 25;
|
|
180
|
+
}
|
|
181
|
+
`,
|
|
182
|
+
|
|
183
|
+
Templates:
|
|
184
|
+
[
|
|
185
|
+
{
|
|
186
|
+
Hash: 'Flow-Container-Template',
|
|
187
|
+
Template: /*html*/`
|
|
188
|
+
<div class="pict-flow-container" id="Flow-Wrapper-{~D:Record.ViewIdentifier~}">
|
|
189
|
+
<div id="Flow-Toolbar-{~D:Record.ViewIdentifier~}"></div>
|
|
190
|
+
<div id="Flow-SVG-Container-{~D:Record.ViewIdentifier~}">
|
|
191
|
+
<svg class="pict-flow-svg"
|
|
192
|
+
id="Flow-SVG-{~D:Record.ViewIdentifier~}"
|
|
193
|
+
xmlns="http://www.w3.org/2000/svg">
|
|
194
|
+
<defs>
|
|
195
|
+
<marker id="flow-arrowhead-{~D:Record.ViewIdentifier~}"
|
|
196
|
+
markerWidth="5" markerHeight="7"
|
|
197
|
+
refX="7.5" refY="3.5"
|
|
198
|
+
orient="auto" markerUnits="strokeWidth">
|
|
199
|
+
<polygon points="0 0, 5 3.5, 0 7" fill="#95a5a6" />
|
|
200
|
+
</marker>
|
|
201
|
+
<marker id="flow-arrowhead-selected-{~D:Record.ViewIdentifier~}"
|
|
202
|
+
markerWidth="5" markerHeight="7"
|
|
203
|
+
refX="7.5" refY="3.5"
|
|
204
|
+
orient="auto" markerUnits="strokeWidth">
|
|
205
|
+
<polygon points="0 0, 5 3.5, 0 7" fill="#3498db" />
|
|
206
|
+
</marker>
|
|
207
|
+
<pattern id="flow-grid-{~D:Record.ViewIdentifier~}"
|
|
208
|
+
width="20" height="20" patternUnits="userSpaceOnUse">
|
|
209
|
+
<line x1="20" y1="0" x2="20" y2="20" class="pict-flow-grid-pattern" />
|
|
210
|
+
<line x1="0" y1="20" x2="20" y2="20" class="pict-flow-grid-pattern" />
|
|
211
|
+
</pattern>
|
|
212
|
+
</defs>
|
|
213
|
+
<rect width="10000" height="10000" x="-5000" y="-5000"
|
|
214
|
+
fill="url(#flow-grid-{~D:Record.ViewIdentifier~})"
|
|
215
|
+
class="pict-flow-grid-background" />
|
|
216
|
+
<g class="pict-flow-viewport" id="Flow-Viewport-{~D:Record.ViewIdentifier~}">
|
|
217
|
+
<g class="pict-flow-connections-layer" id="Flow-Connections-{~D:Record.ViewIdentifier~}"></g>
|
|
218
|
+
<g class="pict-flow-nodes-layer" id="Flow-Nodes-{~D:Record.ViewIdentifier~}"></g>
|
|
219
|
+
</g>
|
|
220
|
+
</svg>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
`
|
|
224
|
+
}
|
|
225
|
+
],
|
|
226
|
+
|
|
227
|
+
Renderables:
|
|
228
|
+
[
|
|
229
|
+
{
|
|
230
|
+
RenderableHash: 'Flow-Container',
|
|
231
|
+
TemplateHash: 'Flow-Container-Template',
|
|
232
|
+
DestinationAddress: '#Flow-Container',
|
|
233
|
+
RenderMethod: 'replace'
|
|
234
|
+
}
|
|
235
|
+
]
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
class PictViewFlow extends libPictView
|
|
239
|
+
{
|
|
240
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
241
|
+
{
|
|
242
|
+
let tmpOptions = Object.assign({}, JSON.parse(JSON.stringify(_DefaultConfiguration)), pOptions);
|
|
243
|
+
super(pFable, tmpOptions, pServiceHash);
|
|
244
|
+
|
|
245
|
+
this.serviceType = 'PictSectionFlow';
|
|
246
|
+
|
|
247
|
+
// Register service types with fable so they can be instantiated
|
|
248
|
+
if (!this.fable.servicesMap.hasOwnProperty('PictServiceFlowInteractionManager'))
|
|
249
|
+
{
|
|
250
|
+
this.fable.addServiceType('PictServiceFlowInteractionManager', libPictServiceFlowInteractionManager);
|
|
251
|
+
}
|
|
252
|
+
if (!this.fable.servicesMap.hasOwnProperty('PictServiceFlowConnectionRenderer'))
|
|
253
|
+
{
|
|
254
|
+
this.fable.addServiceType('PictServiceFlowConnectionRenderer', libPictServiceFlowConnectionRenderer);
|
|
255
|
+
}
|
|
256
|
+
if (!this.fable.servicesMap.hasOwnProperty('PictServiceFlowLayout'))
|
|
257
|
+
{
|
|
258
|
+
this.fable.addServiceType('PictServiceFlowLayout', libPictServiceFlowLayout);
|
|
259
|
+
}
|
|
260
|
+
if (!this.fable.servicesMap.hasOwnProperty('PictProviderFlowNodeTypes'))
|
|
261
|
+
{
|
|
262
|
+
this.fable.addServiceType('PictProviderFlowNodeTypes', libPictProviderFlowNodeTypes);
|
|
263
|
+
}
|
|
264
|
+
if (!this.fable.servicesMap.hasOwnProperty('PictProviderFlowEventHandler'))
|
|
265
|
+
{
|
|
266
|
+
this.fable.addServiceType('PictProviderFlowEventHandler', libPictProviderFlowEventHandler);
|
|
267
|
+
}
|
|
268
|
+
if (!this.fable.servicesMap.hasOwnProperty('PictViewFlowNode'))
|
|
269
|
+
{
|
|
270
|
+
this.fable.addServiceType('PictViewFlowNode', libPictViewFlowNode);
|
|
271
|
+
}
|
|
272
|
+
if (!this.fable.servicesMap.hasOwnProperty('PictViewFlowToolbar'))
|
|
273
|
+
{
|
|
274
|
+
this.fable.addServiceType('PictViewFlowToolbar', libPictViewFlowToolbar);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// Internal state
|
|
278
|
+
this._FlowData = {
|
|
279
|
+
Nodes: [],
|
|
280
|
+
Connections: [],
|
|
281
|
+
ViewState: {
|
|
282
|
+
PanX: 0,
|
|
283
|
+
PanY: 0,
|
|
284
|
+
Zoom: 1,
|
|
285
|
+
SelectedNodeHash: null,
|
|
286
|
+
SelectedConnectionHash: null
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
this._SVGElement = null;
|
|
291
|
+
this._ViewportElement = null;
|
|
292
|
+
this._NodesLayer = null;
|
|
293
|
+
this._ConnectionsLayer = null;
|
|
294
|
+
|
|
295
|
+
this._InteractionManager = null;
|
|
296
|
+
this._ConnectionRenderer = null;
|
|
297
|
+
this._LayoutService = null;
|
|
298
|
+
this._NodeTypeProvider = null;
|
|
299
|
+
this._EventHandlerProvider = null;
|
|
300
|
+
this._NodeView = null;
|
|
301
|
+
this._ToolbarView = null;
|
|
302
|
+
|
|
303
|
+
this.initialRenderComplete = false;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
get flowData()
|
|
307
|
+
{
|
|
308
|
+
return this._FlowData;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
get viewState()
|
|
312
|
+
{
|
|
313
|
+
return this._FlowData.ViewState;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Override render to pass view options as the template record,
|
|
318
|
+
* so template expressions like {~D:Record.ViewIdentifier~} resolve correctly.
|
|
319
|
+
*/
|
|
320
|
+
render(pRenderableHash, pRenderDestinationAddress)
|
|
321
|
+
{
|
|
322
|
+
return super.render(pRenderableHash, pRenderDestinationAddress, this.options);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
renderAsync(pRenderableHash, pRenderDestinationAddress, pTemplateRecordAddress, pRootRenderable, fCallback)
|
|
326
|
+
{
|
|
327
|
+
// If no record address is explicitly provided, use this.options as the record
|
|
328
|
+
if (typeof pTemplateRecordAddress === 'function' || typeof pTemplateRecordAddress === 'undefined')
|
|
329
|
+
{
|
|
330
|
+
return super.renderAsync(pRenderableHash, pRenderDestinationAddress, this.options, pRootRenderable, pTemplateRecordAddress || fCallback);
|
|
331
|
+
}
|
|
332
|
+
return super.renderAsync(pRenderableHash, pRenderDestinationAddress, pTemplateRecordAddress, pRootRenderable, fCallback);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
onBeforeInitialize()
|
|
336
|
+
{
|
|
337
|
+
super.onBeforeInitialize();
|
|
338
|
+
|
|
339
|
+
// Register services
|
|
340
|
+
this._InteractionManager = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowInteractionManager', { FlowView: this });
|
|
341
|
+
this._ConnectionRenderer = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowConnectionRenderer', { FlowView: this });
|
|
342
|
+
this._LayoutService = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowLayout', { FlowView: this });
|
|
343
|
+
|
|
344
|
+
// Register providers
|
|
345
|
+
this._NodeTypeProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowNodeTypes', { FlowView: this });
|
|
346
|
+
this._EventHandlerProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowEventHandler', { FlowView: this });
|
|
347
|
+
|
|
348
|
+
return super.onBeforeInitialize();
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent)
|
|
352
|
+
{
|
|
353
|
+
if (!this.initialRenderComplete)
|
|
354
|
+
{
|
|
355
|
+
this.onAfterInitialRender();
|
|
356
|
+
this.initialRenderComplete = true;
|
|
357
|
+
}
|
|
358
|
+
return super.onAfterRender(pRenderable, pRenderDestinationAddress, pRecord, pContent);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
onAfterInitialRender()
|
|
362
|
+
{
|
|
363
|
+
let tmpViewIdentifier = this.options.ViewIdentifier;
|
|
364
|
+
|
|
365
|
+
// Grab SVG DOM elements
|
|
366
|
+
let tmpSVGElements = this.pict.ContentAssignment.getElement(`#Flow-SVG-${tmpViewIdentifier}`);
|
|
367
|
+
if (tmpSVGElements.length < 1)
|
|
368
|
+
{
|
|
369
|
+
this.log.error(`PictSectionFlow could not find SVG element #Flow-SVG-${tmpViewIdentifier}`);
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
this._SVGElement = tmpSVGElements[0];
|
|
373
|
+
|
|
374
|
+
let tmpViewportElements = this.pict.ContentAssignment.getElement(`#Flow-Viewport-${tmpViewIdentifier}`);
|
|
375
|
+
if (tmpViewportElements.length > 0)
|
|
376
|
+
{
|
|
377
|
+
this._ViewportElement = tmpViewportElements[0];
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
let tmpNodesElements = this.pict.ContentAssignment.getElement(`#Flow-Nodes-${tmpViewIdentifier}`);
|
|
381
|
+
if (tmpNodesElements.length > 0)
|
|
382
|
+
{
|
|
383
|
+
this._NodesLayer = tmpNodesElements[0];
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
let tmpConnectionsElements = this.pict.ContentAssignment.getElement(`#Flow-Connections-${tmpViewIdentifier}`);
|
|
387
|
+
if (tmpConnectionsElements.length > 0)
|
|
388
|
+
{
|
|
389
|
+
this._ConnectionsLayer = tmpConnectionsElements[0];
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// Initialize services with references
|
|
393
|
+
if (!this._InteractionManager)
|
|
394
|
+
{
|
|
395
|
+
this._InteractionManager = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowInteractionManager', { FlowView: this });
|
|
396
|
+
}
|
|
397
|
+
if (!this._ConnectionRenderer)
|
|
398
|
+
{
|
|
399
|
+
this._ConnectionRenderer = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowConnectionRenderer', { FlowView: this });
|
|
400
|
+
}
|
|
401
|
+
if (!this._LayoutService)
|
|
402
|
+
{
|
|
403
|
+
this._LayoutService = this.fable.instantiateServiceProviderWithoutRegistration('PictServiceFlowLayout', { FlowView: this });
|
|
404
|
+
}
|
|
405
|
+
if (!this._NodeTypeProvider)
|
|
406
|
+
{
|
|
407
|
+
this._NodeTypeProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowNodeTypes', { FlowView: this });
|
|
408
|
+
}
|
|
409
|
+
if (!this._EventHandlerProvider)
|
|
410
|
+
{
|
|
411
|
+
this._EventHandlerProvider = this.fable.instantiateServiceProviderWithoutRegistration('PictProviderFlowEventHandler', { FlowView: this });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Setup the toolbar if enabled
|
|
415
|
+
if (this.options.EnableToolbar)
|
|
416
|
+
{
|
|
417
|
+
this._ToolbarView = this.fable.instantiateServiceProviderWithoutRegistration('PictViewFlowToolbar',
|
|
418
|
+
Object.assign({},
|
|
419
|
+
libPictViewFlowToolbar.default_configuration,
|
|
420
|
+
{
|
|
421
|
+
ViewIdentifier: `Flow-Toolbar-${tmpViewIdentifier}`,
|
|
422
|
+
DefaultDestinationAddress: `#Flow-Toolbar-${tmpViewIdentifier}`,
|
|
423
|
+
FlowViewIdentifier: tmpViewIdentifier
|
|
424
|
+
}
|
|
425
|
+
));
|
|
426
|
+
// Use the toolbar's render method after it's set up
|
|
427
|
+
if (this._ToolbarView && typeof this._ToolbarView.render === 'function')
|
|
428
|
+
{
|
|
429
|
+
this._ToolbarView._FlowView = this;
|
|
430
|
+
this._ToolbarView.render();
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// Setup the node renderer
|
|
435
|
+
this._NodeView = this.fable.instantiateServiceProviderWithoutRegistration('PictViewFlowNode',
|
|
436
|
+
Object.assign({},
|
|
437
|
+
libPictViewFlowNode.default_configuration,
|
|
438
|
+
{
|
|
439
|
+
ViewIdentifier: `Flow-NodeRenderer-${tmpViewIdentifier}`,
|
|
440
|
+
AutoRender: false
|
|
441
|
+
}
|
|
442
|
+
));
|
|
443
|
+
this._NodeView._FlowView = this;
|
|
444
|
+
|
|
445
|
+
// Bind interaction events
|
|
446
|
+
this._InteractionManager.initialize(this._SVGElement, this._ViewportElement);
|
|
447
|
+
|
|
448
|
+
// Load initial flow data if an address is configured
|
|
449
|
+
if (this.options.FlowDataAddress)
|
|
450
|
+
{
|
|
451
|
+
this.marshalToView();
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Render the initial flow
|
|
455
|
+
this.renderFlow();
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Marshal data from AppData into the flow view
|
|
460
|
+
*/
|
|
461
|
+
marshalToView()
|
|
462
|
+
{
|
|
463
|
+
if (this.options.FlowDataAddress)
|
|
464
|
+
{
|
|
465
|
+
const tmpAddressSpace =
|
|
466
|
+
{
|
|
467
|
+
Fable: this.fable,
|
|
468
|
+
Pict: this.pict || this.fable,
|
|
469
|
+
AppData: this.pict ? this.pict.AppData : this.fable.AppData,
|
|
470
|
+
Bundle: this.Bundle,
|
|
471
|
+
Options: this.options
|
|
472
|
+
};
|
|
473
|
+
let tmpData = this.fable.manifest.getValueByHash(tmpAddressSpace, this.options.FlowDataAddress);
|
|
474
|
+
if (typeof tmpData === 'object' && tmpData !== null)
|
|
475
|
+
{
|
|
476
|
+
this.setFlowData(tmpData);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Marshal data from the flow view back to AppData
|
|
483
|
+
*/
|
|
484
|
+
marshalFromView()
|
|
485
|
+
{
|
|
486
|
+
if (this.options.FlowDataAddress)
|
|
487
|
+
{
|
|
488
|
+
const tmpAddressSpace =
|
|
489
|
+
{
|
|
490
|
+
Fable: this.fable,
|
|
491
|
+
Pict: this.pict || this.fable,
|
|
492
|
+
AppData: this.pict ? this.pict.AppData : this.fable.AppData,
|
|
493
|
+
Bundle: this.Bundle,
|
|
494
|
+
Options: this.options
|
|
495
|
+
};
|
|
496
|
+
this.fable.manifest.setValueByHash(tmpAddressSpace, this.options.FlowDataAddress, JSON.parse(JSON.stringify(this._FlowData)));
|
|
497
|
+
}
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Get the complete flow data object
|
|
502
|
+
* @returns {Object} The flow data including nodes, connections, and view state
|
|
503
|
+
*/
|
|
504
|
+
getFlowData()
|
|
505
|
+
{
|
|
506
|
+
return JSON.parse(JSON.stringify(this._FlowData));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Set the complete flow data object and re-render
|
|
511
|
+
* @param {Object} pFlowData - The flow data to set
|
|
512
|
+
*/
|
|
513
|
+
setFlowData(pFlowData)
|
|
514
|
+
{
|
|
515
|
+
if (typeof pFlowData !== 'object' || pFlowData === null)
|
|
516
|
+
{
|
|
517
|
+
this.log.warn('PictSectionFlow setFlowData received invalid data');
|
|
518
|
+
return;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
this._FlowData = {
|
|
522
|
+
Nodes: Array.isArray(pFlowData.Nodes) ? pFlowData.Nodes : [],
|
|
523
|
+
Connections: Array.isArray(pFlowData.Connections) ? pFlowData.Connections : [],
|
|
524
|
+
ViewState: Object.assign(
|
|
525
|
+
{ PanX: 0, PanY: 0, Zoom: 1, SelectedNodeHash: null, SelectedConnectionHash: null },
|
|
526
|
+
pFlowData.ViewState || {}
|
|
527
|
+
)
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
if (this.initialRenderComplete)
|
|
531
|
+
{
|
|
532
|
+
this.renderFlow();
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Add a new node to the flow
|
|
538
|
+
* @param {string} pType - The node type hash
|
|
539
|
+
* @param {number} pX - X position
|
|
540
|
+
* @param {number} pY - Y position
|
|
541
|
+
* @param {string} [pTitle] - Optional title
|
|
542
|
+
* @param {Object} [pData] - Optional additional data
|
|
543
|
+
* @returns {Object} The created node
|
|
544
|
+
*/
|
|
545
|
+
addNode(pType, pX, pY, pTitle, pData)
|
|
546
|
+
{
|
|
547
|
+
let tmpType = pType || this.options.DefaultNodeType;
|
|
548
|
+
let tmpNodeTypeConfig = this._NodeTypeProvider.getNodeType(tmpType);
|
|
549
|
+
|
|
550
|
+
let tmpNodeHash = `node-${this.fable.getUUID()}`;
|
|
551
|
+
let tmpNode =
|
|
552
|
+
{
|
|
553
|
+
Hash: tmpNodeHash,
|
|
554
|
+
Type: tmpType,
|
|
555
|
+
X: pX || 100,
|
|
556
|
+
Y: pY || 100,
|
|
557
|
+
Width: (tmpNodeTypeConfig && tmpNodeTypeConfig.DefaultWidth) || this.options.DefaultNodeWidth,
|
|
558
|
+
Height: (tmpNodeTypeConfig && tmpNodeTypeConfig.DefaultHeight) || this.options.DefaultNodeHeight,
|
|
559
|
+
Title: pTitle || (tmpNodeTypeConfig && tmpNodeTypeConfig.Label) || 'New Node',
|
|
560
|
+
Ports: (tmpNodeTypeConfig && tmpNodeTypeConfig.DefaultPorts)
|
|
561
|
+
? JSON.parse(JSON.stringify(tmpNodeTypeConfig.DefaultPorts))
|
|
562
|
+
: [
|
|
563
|
+
{ Hash: `port-in-${this.fable.getUUID()}`, Direction: 'input', Side: 'left', Label: 'In' },
|
|
564
|
+
{ Hash: `port-out-${this.fable.getUUID()}`, Direction: 'output', Side: 'right', Label: 'Out' }
|
|
565
|
+
],
|
|
566
|
+
Data: pData || {}
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
// Ensure each port has a unique hash
|
|
570
|
+
for (let i = 0; i < tmpNode.Ports.length; i++)
|
|
571
|
+
{
|
|
572
|
+
if (!tmpNode.Ports[i].Hash)
|
|
573
|
+
{
|
|
574
|
+
tmpNode.Ports[i].Hash = `port-${tmpNode.Ports[i].Direction}-${this.fable.getUUID()}`;
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
this._FlowData.Nodes.push(tmpNode);
|
|
579
|
+
this.renderFlow();
|
|
580
|
+
this.marshalFromView();
|
|
581
|
+
|
|
582
|
+
if (this._EventHandlerProvider)
|
|
583
|
+
{
|
|
584
|
+
this._EventHandlerProvider.fireEvent('onNodeAdded', tmpNode);
|
|
585
|
+
this._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowData);
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
return tmpNode;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* Remove a node and all its connections
|
|
593
|
+
* @param {string} pNodeHash - The hash of the node to remove
|
|
594
|
+
* @returns {boolean} Whether the node was removed
|
|
595
|
+
*/
|
|
596
|
+
removeNode(pNodeHash)
|
|
597
|
+
{
|
|
598
|
+
let tmpNodeIndex = this._FlowData.Nodes.findIndex((pNode) => pNode.Hash === pNodeHash);
|
|
599
|
+
if (tmpNodeIndex < 0)
|
|
600
|
+
{
|
|
601
|
+
this.log.warn(`PictSectionFlow removeNode: node ${pNodeHash} not found`);
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
let tmpRemovedNode = this._FlowData.Nodes.splice(tmpNodeIndex, 1)[0];
|
|
606
|
+
|
|
607
|
+
// Remove all connections involving this node
|
|
608
|
+
this._FlowData.Connections = this._FlowData.Connections.filter((pConnection) =>
|
|
609
|
+
{
|
|
610
|
+
return pConnection.SourceNodeHash !== pNodeHash && pConnection.TargetNodeHash !== pNodeHash;
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
// Clear selection if this node was selected
|
|
614
|
+
if (this._FlowData.ViewState.SelectedNodeHash === pNodeHash)
|
|
615
|
+
{
|
|
616
|
+
this._FlowData.ViewState.SelectedNodeHash = null;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
this.renderFlow();
|
|
620
|
+
this.marshalFromView();
|
|
621
|
+
|
|
622
|
+
if (this._EventHandlerProvider)
|
|
623
|
+
{
|
|
624
|
+
this._EventHandlerProvider.fireEvent('onNodeRemoved', tmpRemovedNode);
|
|
625
|
+
this._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowData);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
return true;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Add a connection between two ports
|
|
633
|
+
* @param {string} pSourceNodeHash
|
|
634
|
+
* @param {string} pSourcePortHash
|
|
635
|
+
* @param {string} pTargetNodeHash
|
|
636
|
+
* @param {string} pTargetPortHash
|
|
637
|
+
* @param {Object} [pData] - Optional additional data
|
|
638
|
+
* @returns {Object|false} The created connection, or false if invalid
|
|
639
|
+
*/
|
|
640
|
+
addConnection(pSourceNodeHash, pSourcePortHash, pTargetNodeHash, pTargetPortHash, pData)
|
|
641
|
+
{
|
|
642
|
+
// Validate that both nodes and ports exist
|
|
643
|
+
let tmpSourceNode = this._FlowData.Nodes.find((pNode) => pNode.Hash === pSourceNodeHash);
|
|
644
|
+
let tmpTargetNode = this._FlowData.Nodes.find((pNode) => pNode.Hash === pTargetNodeHash);
|
|
645
|
+
|
|
646
|
+
if (!tmpSourceNode || !tmpTargetNode)
|
|
647
|
+
{
|
|
648
|
+
this.log.warn('PictSectionFlow addConnection: source or target node not found');
|
|
649
|
+
return false;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
let tmpSourcePort = tmpSourceNode.Ports.find((pPort) => pPort.Hash === pSourcePortHash);
|
|
653
|
+
let tmpTargetPort = tmpTargetNode.Ports.find((pPort) => pPort.Hash === pTargetPortHash);
|
|
654
|
+
|
|
655
|
+
if (!tmpSourcePort || !tmpTargetPort)
|
|
656
|
+
{
|
|
657
|
+
this.log.warn('PictSectionFlow addConnection: source or target port not found');
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// Prevent self-connections
|
|
662
|
+
if (pSourceNodeHash === pTargetNodeHash)
|
|
663
|
+
{
|
|
664
|
+
this.log.warn('PictSectionFlow addConnection: cannot connect a node to itself');
|
|
665
|
+
return false;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// Check for duplicate connections
|
|
669
|
+
let tmpDuplicate = this._FlowData.Connections.find((pConn) =>
|
|
670
|
+
{
|
|
671
|
+
return pConn.SourceNodeHash === pSourceNodeHash
|
|
672
|
+
&& pConn.SourcePortHash === pSourcePortHash
|
|
673
|
+
&& pConn.TargetNodeHash === pTargetNodeHash
|
|
674
|
+
&& pConn.TargetPortHash === pTargetPortHash;
|
|
675
|
+
});
|
|
676
|
+
if (tmpDuplicate)
|
|
677
|
+
{
|
|
678
|
+
this.log.warn('PictSectionFlow addConnection: duplicate connection');
|
|
679
|
+
return false;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
let tmpConnection =
|
|
683
|
+
{
|
|
684
|
+
Hash: `conn-${this.fable.getUUID()}`,
|
|
685
|
+
SourceNodeHash: pSourceNodeHash,
|
|
686
|
+
SourcePortHash: pSourcePortHash,
|
|
687
|
+
TargetNodeHash: pTargetNodeHash,
|
|
688
|
+
TargetPortHash: pTargetPortHash,
|
|
689
|
+
Data: pData || {}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
this._FlowData.Connections.push(tmpConnection);
|
|
693
|
+
this.renderFlow();
|
|
694
|
+
this.marshalFromView();
|
|
695
|
+
|
|
696
|
+
if (this._EventHandlerProvider)
|
|
697
|
+
{
|
|
698
|
+
this._EventHandlerProvider.fireEvent('onConnectionCreated', tmpConnection);
|
|
699
|
+
this._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowData);
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
return tmpConnection;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/**
|
|
706
|
+
* Remove a connection
|
|
707
|
+
* @param {string} pConnectionHash - The hash of the connection to remove
|
|
708
|
+
* @returns {boolean} Whether the connection was removed
|
|
709
|
+
*/
|
|
710
|
+
removeConnection(pConnectionHash)
|
|
711
|
+
{
|
|
712
|
+
let tmpConnectionIndex = this._FlowData.Connections.findIndex((pConn) => pConn.Hash === pConnectionHash);
|
|
713
|
+
if (tmpConnectionIndex < 0)
|
|
714
|
+
{
|
|
715
|
+
this.log.warn(`PictSectionFlow removeConnection: connection ${pConnectionHash} not found`);
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
let tmpRemovedConnection = this._FlowData.Connections.splice(tmpConnectionIndex, 1)[0];
|
|
720
|
+
|
|
721
|
+
if (this._FlowData.ViewState.SelectedConnectionHash === pConnectionHash)
|
|
722
|
+
{
|
|
723
|
+
this._FlowData.ViewState.SelectedConnectionHash = null;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
this.renderFlow();
|
|
727
|
+
this.marshalFromView();
|
|
728
|
+
|
|
729
|
+
if (this._EventHandlerProvider)
|
|
730
|
+
{
|
|
731
|
+
this._EventHandlerProvider.fireEvent('onConnectionRemoved', tmpRemovedConnection);
|
|
732
|
+
this._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowData);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return true;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Select a node
|
|
740
|
+
* @param {string|null} pNodeHash - Hash of the node to select, or null to deselect
|
|
741
|
+
*/
|
|
742
|
+
selectNode(pNodeHash)
|
|
743
|
+
{
|
|
744
|
+
let tmpPreviousSelection = this._FlowData.ViewState.SelectedNodeHash;
|
|
745
|
+
this._FlowData.ViewState.SelectedNodeHash = pNodeHash;
|
|
746
|
+
this._FlowData.ViewState.SelectedConnectionHash = null;
|
|
747
|
+
|
|
748
|
+
this.renderFlow();
|
|
749
|
+
|
|
750
|
+
if (this._EventHandlerProvider && pNodeHash !== tmpPreviousSelection)
|
|
751
|
+
{
|
|
752
|
+
let tmpNode = pNodeHash ? this._FlowData.Nodes.find((pNode) => pNode.Hash === pNodeHash) : null;
|
|
753
|
+
this._EventHandlerProvider.fireEvent('onNodeSelected', tmpNode);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Select a connection
|
|
759
|
+
* @param {string|null} pConnectionHash - Hash of the connection to select, or null to deselect
|
|
760
|
+
*/
|
|
761
|
+
selectConnection(pConnectionHash)
|
|
762
|
+
{
|
|
763
|
+
let tmpPreviousSelection = this._FlowData.ViewState.SelectedConnectionHash;
|
|
764
|
+
this._FlowData.ViewState.SelectedConnectionHash = pConnectionHash;
|
|
765
|
+
this._FlowData.ViewState.SelectedNodeHash = null;
|
|
766
|
+
|
|
767
|
+
this.renderFlow();
|
|
768
|
+
|
|
769
|
+
if (this._EventHandlerProvider && pConnectionHash !== tmpPreviousSelection)
|
|
770
|
+
{
|
|
771
|
+
let tmpConnection = pConnectionHash ? this._FlowData.Connections.find((pConn) => pConn.Hash === pConnectionHash) : null;
|
|
772
|
+
this._EventHandlerProvider.fireEvent('onConnectionSelected', tmpConnection);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
/**
|
|
777
|
+
* Deselect all nodes and connections
|
|
778
|
+
*/
|
|
779
|
+
deselectAll()
|
|
780
|
+
{
|
|
781
|
+
this._FlowData.ViewState.SelectedNodeHash = null;
|
|
782
|
+
this._FlowData.ViewState.SelectedConnectionHash = null;
|
|
783
|
+
this.renderFlow();
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
/**
|
|
787
|
+
* Delete the currently selected node or connection
|
|
788
|
+
* @returns {boolean}
|
|
789
|
+
*/
|
|
790
|
+
deleteSelected()
|
|
791
|
+
{
|
|
792
|
+
if (this._FlowData.ViewState.SelectedNodeHash)
|
|
793
|
+
{
|
|
794
|
+
return this.removeNode(this._FlowData.ViewState.SelectedNodeHash);
|
|
795
|
+
}
|
|
796
|
+
if (this._FlowData.ViewState.SelectedConnectionHash)
|
|
797
|
+
{
|
|
798
|
+
return this.removeConnection(this._FlowData.ViewState.SelectedConnectionHash);
|
|
799
|
+
}
|
|
800
|
+
return false;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Update the viewport transform (pan and zoom)
|
|
805
|
+
*/
|
|
806
|
+
updateViewportTransform()
|
|
807
|
+
{
|
|
808
|
+
if (!this._ViewportElement) return;
|
|
809
|
+
let tmpVS = this._FlowData.ViewState;
|
|
810
|
+
this._ViewportElement.setAttribute('transform',
|
|
811
|
+
`translate(${tmpVS.PanX}, ${tmpVS.PanY}) scale(${tmpVS.Zoom})`
|
|
812
|
+
);
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Set zoom level
|
|
817
|
+
* @param {number} pZoom - The zoom level
|
|
818
|
+
* @param {number} [pFocusX] - X coordinate to zoom toward (SVG space)
|
|
819
|
+
* @param {number} [pFocusY] - Y coordinate to zoom toward (SVG space)
|
|
820
|
+
*/
|
|
821
|
+
setZoom(pZoom, pFocusX, pFocusY)
|
|
822
|
+
{
|
|
823
|
+
let tmpNewZoom = Math.max(this.options.MinZoom, Math.min(this.options.MaxZoom, pZoom));
|
|
824
|
+
let tmpOldZoom = this._FlowData.ViewState.Zoom;
|
|
825
|
+
|
|
826
|
+
if (typeof pFocusX === 'number' && typeof pFocusY === 'number')
|
|
827
|
+
{
|
|
828
|
+
// Zoom toward focus point
|
|
829
|
+
let tmpVS = this._FlowData.ViewState;
|
|
830
|
+
tmpVS.PanX = pFocusX - (pFocusX - tmpVS.PanX) * (tmpNewZoom / tmpOldZoom);
|
|
831
|
+
tmpVS.PanY = pFocusY - (pFocusY - tmpVS.PanY) * (tmpNewZoom / tmpOldZoom);
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
this._FlowData.ViewState.Zoom = tmpNewZoom;
|
|
835
|
+
this.updateViewportTransform();
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
/**
|
|
839
|
+
* Zoom to fit all nodes in the viewport
|
|
840
|
+
*/
|
|
841
|
+
zoomToFit()
|
|
842
|
+
{
|
|
843
|
+
if (this._FlowData.Nodes.length === 0) return;
|
|
844
|
+
if (!this._SVGElement) return;
|
|
845
|
+
|
|
846
|
+
let tmpMinX = Infinity, tmpMinY = Infinity;
|
|
847
|
+
let tmpMaxX = -Infinity, tmpMaxY = -Infinity;
|
|
848
|
+
|
|
849
|
+
for (let i = 0; i < this._FlowData.Nodes.length; i++)
|
|
850
|
+
{
|
|
851
|
+
let tmpNode = this._FlowData.Nodes[i];
|
|
852
|
+
tmpMinX = Math.min(tmpMinX, tmpNode.X);
|
|
853
|
+
tmpMinY = Math.min(tmpMinY, tmpNode.Y);
|
|
854
|
+
tmpMaxX = Math.max(tmpMaxX, tmpNode.X + tmpNode.Width);
|
|
855
|
+
tmpMaxY = Math.max(tmpMaxY, tmpNode.Y + tmpNode.Height);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
let tmpPadding = 50;
|
|
859
|
+
let tmpFlowWidth = tmpMaxX - tmpMinX + tmpPadding * 2;
|
|
860
|
+
let tmpFlowHeight = tmpMaxY - tmpMinY + tmpPadding * 2;
|
|
861
|
+
|
|
862
|
+
let tmpSVGRect = this._SVGElement.getBoundingClientRect();
|
|
863
|
+
let tmpScaleX = tmpSVGRect.width / tmpFlowWidth;
|
|
864
|
+
let tmpScaleY = tmpSVGRect.height / tmpFlowHeight;
|
|
865
|
+
let tmpZoom = Math.min(tmpScaleX, tmpScaleY, 1.0); // Don't zoom in past 1.0
|
|
866
|
+
tmpZoom = Math.max(this.options.MinZoom, Math.min(this.options.MaxZoom, tmpZoom));
|
|
867
|
+
|
|
868
|
+
let tmpCenterX = (tmpMinX + tmpMaxX) / 2;
|
|
869
|
+
let tmpCenterY = (tmpMinY + tmpMaxY) / 2;
|
|
870
|
+
|
|
871
|
+
this._FlowData.ViewState.Zoom = tmpZoom;
|
|
872
|
+
this._FlowData.ViewState.PanX = (tmpSVGRect.width / 2) - (tmpCenterX * tmpZoom);
|
|
873
|
+
this._FlowData.ViewState.PanY = (tmpSVGRect.height / 2) - (tmpCenterY * tmpZoom);
|
|
874
|
+
|
|
875
|
+
this.updateViewportTransform();
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Apply auto-layout to all nodes
|
|
880
|
+
*/
|
|
881
|
+
autoLayout()
|
|
882
|
+
{
|
|
883
|
+
if (this._LayoutService)
|
|
884
|
+
{
|
|
885
|
+
this._LayoutService.autoLayout(this._FlowData.Nodes, this._FlowData.Connections);
|
|
886
|
+
this.renderFlow();
|
|
887
|
+
this.marshalFromView();
|
|
888
|
+
|
|
889
|
+
if (this._EventHandlerProvider)
|
|
890
|
+
{
|
|
891
|
+
this._EventHandlerProvider.fireEvent('onFlowChanged', this._FlowData);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
/**
|
|
897
|
+
* Get a node by hash
|
|
898
|
+
* @param {string} pNodeHash
|
|
899
|
+
* @returns {Object|null}
|
|
900
|
+
*/
|
|
901
|
+
getNode(pNodeHash)
|
|
902
|
+
{
|
|
903
|
+
return this._FlowData.Nodes.find((pNode) => pNode.Hash === pNodeHash) || null;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/**
|
|
907
|
+
* Get a connection by hash
|
|
908
|
+
* @param {string} pConnectionHash
|
|
909
|
+
* @returns {Object|null}
|
|
910
|
+
*/
|
|
911
|
+
getConnection(pConnectionHash)
|
|
912
|
+
{
|
|
913
|
+
return this._FlowData.Connections.find((pConn) => pConn.Hash === pConnectionHash) || null;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
/**
|
|
917
|
+
* Get a port's absolute position in SVG coordinates.
|
|
918
|
+
*
|
|
919
|
+
* For left and right side ports, positioning is offset below the title bar
|
|
920
|
+
* so that connection endpoints match the rendered port circles.
|
|
921
|
+
*
|
|
922
|
+
* @param {string} pNodeHash
|
|
923
|
+
* @param {string} pPortHash
|
|
924
|
+
* @returns {{x: number, y: number, side: string}|null}
|
|
925
|
+
*/
|
|
926
|
+
getPortPosition(pNodeHash, pPortHash)
|
|
927
|
+
{
|
|
928
|
+
let tmpNode = this.getNode(pNodeHash);
|
|
929
|
+
if (!tmpNode) return null;
|
|
930
|
+
|
|
931
|
+
let tmpPort = tmpNode.Ports.find((p) => p.Hash === pPortHash);
|
|
932
|
+
if (!tmpPort) return null;
|
|
933
|
+
|
|
934
|
+
// Count ports on the same side (matching both direction and side)
|
|
935
|
+
let tmpSameSidePorts = tmpNode.Ports.filter((p) => p.Side === tmpPort.Side);
|
|
936
|
+
let tmpPortIndex = tmpSameSidePorts.indexOf(tmpPort);
|
|
937
|
+
let tmpPortCount = tmpSameSidePorts.length;
|
|
938
|
+
|
|
939
|
+
let tmpTitleBarHeight = (this._NodeView && this._NodeView.options.NodeTitleBarHeight) || 28;
|
|
940
|
+
|
|
941
|
+
let tmpX, tmpY;
|
|
942
|
+
|
|
943
|
+
switch (tmpPort.Side)
|
|
944
|
+
{
|
|
945
|
+
case 'left':
|
|
946
|
+
{
|
|
947
|
+
// Distribute ports in the body area below the title bar
|
|
948
|
+
let tmpBodyHeight = tmpNode.Height - tmpTitleBarHeight;
|
|
949
|
+
tmpX = tmpNode.X;
|
|
950
|
+
tmpY = tmpNode.Y + tmpTitleBarHeight + ((tmpPortIndex + 1) / (tmpPortCount + 1)) * tmpBodyHeight;
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
case 'right':
|
|
954
|
+
{
|
|
955
|
+
let tmpBodyHeight = tmpNode.Height - tmpTitleBarHeight;
|
|
956
|
+
tmpX = tmpNode.X + tmpNode.Width;
|
|
957
|
+
tmpY = tmpNode.Y + tmpTitleBarHeight + ((tmpPortIndex + 1) / (tmpPortCount + 1)) * tmpBodyHeight;
|
|
958
|
+
break;
|
|
959
|
+
}
|
|
960
|
+
case 'top':
|
|
961
|
+
tmpX = tmpNode.X + ((tmpPortIndex + 1) / (tmpPortCount + 1)) * tmpNode.Width;
|
|
962
|
+
tmpY = tmpNode.Y;
|
|
963
|
+
break;
|
|
964
|
+
case 'bottom':
|
|
965
|
+
tmpX = tmpNode.X + ((tmpPortIndex + 1) / (tmpPortCount + 1)) * tmpNode.Width;
|
|
966
|
+
tmpY = tmpNode.Y + tmpNode.Height;
|
|
967
|
+
break;
|
|
968
|
+
default:
|
|
969
|
+
tmpX = tmpNode.X + tmpNode.Width;
|
|
970
|
+
tmpY = tmpNode.Y + tmpNode.Height / 2;
|
|
971
|
+
break;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
return { x: tmpX, y: tmpY, side: tmpPort.Side || 'right' };
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
/**
|
|
978
|
+
* Convert screen coordinates to SVG viewport coordinates
|
|
979
|
+
* @param {number} pScreenX
|
|
980
|
+
* @param {number} pScreenY
|
|
981
|
+
* @returns {{x: number, y: number}}
|
|
982
|
+
*/
|
|
983
|
+
screenToSVGCoords(pScreenX, pScreenY)
|
|
984
|
+
{
|
|
985
|
+
if (!this._SVGElement)
|
|
986
|
+
{
|
|
987
|
+
return { x: pScreenX, y: pScreenY };
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
let tmpPoint = this._SVGElement.createSVGPoint();
|
|
991
|
+
tmpPoint.x = pScreenX;
|
|
992
|
+
tmpPoint.y = pScreenY;
|
|
993
|
+
|
|
994
|
+
let tmpCTM = this._SVGElement.getScreenCTM();
|
|
995
|
+
if (tmpCTM)
|
|
996
|
+
{
|
|
997
|
+
let tmpInverse = tmpCTM.inverse();
|
|
998
|
+
let tmpTransformed = tmpPoint.matrixTransform(tmpInverse);
|
|
999
|
+
// Account for viewport pan/zoom
|
|
1000
|
+
let tmpVS = this._FlowData.ViewState;
|
|
1001
|
+
return {
|
|
1002
|
+
x: (tmpTransformed.x - tmpVS.PanX) / tmpVS.Zoom,
|
|
1003
|
+
y: (tmpTransformed.y - tmpVS.PanY) / tmpVS.Zoom
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return { x: pScreenX, y: pScreenY };
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
/**
|
|
1011
|
+
* Render the complete flow diagram
|
|
1012
|
+
*/
|
|
1013
|
+
renderFlow()
|
|
1014
|
+
{
|
|
1015
|
+
if (!this._NodesLayer || !this._ConnectionsLayer) return;
|
|
1016
|
+
|
|
1017
|
+
// Clear existing SVG content
|
|
1018
|
+
while (this._NodesLayer.firstChild)
|
|
1019
|
+
{
|
|
1020
|
+
this._NodesLayer.removeChild(this._NodesLayer.firstChild);
|
|
1021
|
+
}
|
|
1022
|
+
while (this._ConnectionsLayer.firstChild)
|
|
1023
|
+
{
|
|
1024
|
+
this._ConnectionsLayer.removeChild(this._ConnectionsLayer.firstChild);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Render connections first (behind nodes)
|
|
1028
|
+
for (let i = 0; i < this._FlowData.Connections.length; i++)
|
|
1029
|
+
{
|
|
1030
|
+
let tmpConnection = this._FlowData.Connections[i];
|
|
1031
|
+
let tmpIsSelected = (this._FlowData.ViewState.SelectedConnectionHash === tmpConnection.Hash);
|
|
1032
|
+
|
|
1033
|
+
this._ConnectionRenderer.renderConnection(
|
|
1034
|
+
tmpConnection,
|
|
1035
|
+
this._ConnectionsLayer,
|
|
1036
|
+
tmpIsSelected
|
|
1037
|
+
);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
// Render nodes
|
|
1041
|
+
for (let i = 0; i < this._FlowData.Nodes.length; i++)
|
|
1042
|
+
{
|
|
1043
|
+
let tmpNode = this._FlowData.Nodes[i];
|
|
1044
|
+
let tmpIsSelected = (this._FlowData.ViewState.SelectedNodeHash === tmpNode.Hash);
|
|
1045
|
+
let tmpNodeTypeConfig = this._NodeTypeProvider.getNodeType(tmpNode.Type);
|
|
1046
|
+
|
|
1047
|
+
this._NodeView.renderNode(tmpNode, this._NodesLayer, tmpIsSelected, tmpNodeTypeConfig);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
// Update viewport transform
|
|
1051
|
+
this.updateViewportTransform();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
/**
|
|
1055
|
+
* Update a single node's position in the SVG without full re-render (for drag performance)
|
|
1056
|
+
* @param {string} pNodeHash
|
|
1057
|
+
* @param {number} pX
|
|
1058
|
+
* @param {number} pY
|
|
1059
|
+
*/
|
|
1060
|
+
updateNodePosition(pNodeHash, pX, pY)
|
|
1061
|
+
{
|
|
1062
|
+
let tmpNode = this.getNode(pNodeHash);
|
|
1063
|
+
if (!tmpNode) return;
|
|
1064
|
+
|
|
1065
|
+
if (this.options.EnableGridSnap)
|
|
1066
|
+
{
|
|
1067
|
+
pX = this._LayoutService.snapToGrid(pX, this.options.GridSnapSize);
|
|
1068
|
+
pY = this._LayoutService.snapToGrid(pY, this.options.GridSnapSize);
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
tmpNode.X = pX;
|
|
1072
|
+
tmpNode.Y = pY;
|
|
1073
|
+
|
|
1074
|
+
// Update the node's SVG group transform for smooth dragging
|
|
1075
|
+
let tmpNodeGroup = this._NodesLayer.querySelector(`[data-node-hash="${pNodeHash}"]`);
|
|
1076
|
+
if (tmpNodeGroup)
|
|
1077
|
+
{
|
|
1078
|
+
tmpNodeGroup.setAttribute('transform', `translate(${pX}, ${pY})`);
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
// Re-render connections that involve this node
|
|
1082
|
+
this._renderConnectionsForNode(pNodeHash);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Re-render only connections that involve a specific node (for drag performance)
|
|
1087
|
+
* @param {string} pNodeHash
|
|
1088
|
+
*/
|
|
1089
|
+
_renderConnectionsForNode(pNodeHash)
|
|
1090
|
+
{
|
|
1091
|
+
let tmpAffectedConnections = this._FlowData.Connections.filter((pConn) =>
|
|
1092
|
+
{
|
|
1093
|
+
return pConn.SourceNodeHash === pNodeHash || pConn.TargetNodeHash === pNodeHash;
|
|
1094
|
+
});
|
|
1095
|
+
|
|
1096
|
+
for (let i = 0; i < tmpAffectedConnections.length; i++)
|
|
1097
|
+
{
|
|
1098
|
+
let tmpConn = tmpAffectedConnections[i];
|
|
1099
|
+
let tmpIsSelected = (this._FlowData.ViewState.SelectedConnectionHash === tmpConn.Hash);
|
|
1100
|
+
|
|
1101
|
+
// Remove existing connection SVG elements
|
|
1102
|
+
let tmpExisting = this._ConnectionsLayer.querySelectorAll(`[data-connection-hash="${tmpConn.Hash}"]`);
|
|
1103
|
+
for (let j = 0; j < tmpExisting.length; j++)
|
|
1104
|
+
{
|
|
1105
|
+
tmpExisting[j].remove();
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// Re-render this connection
|
|
1109
|
+
this._ConnectionRenderer.renderConnection(tmpConn, this._ConnectionsLayer, tmpIsSelected);
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
module.exports = PictViewFlow;
|
|
1115
|
+
|
|
1116
|
+
module.exports.default_configuration = _DefaultConfiguration;
|