pict-section-flow 0.0.2 → 0.0.5
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/.claude/launch.json +11 -0
- package/docs/README.md +51 -0
- package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +105 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +36 -0
- package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +42 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Each.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +1 -1
- package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +98 -0
- package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +44 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +1 -1
- package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +9 -1
- package/package.json +7 -5
- package/source/Pict-Section-Flow.js +8 -1
- package/source/PictFlowCard.js +52 -1
- package/source/panels/FlowCardPropertiesPanel-Template.js +1 -1
- package/source/providers/PictProvider-Flow-CSS.js +1440 -0
- package/source/providers/PictProvider-Flow-ConnectorShapes.js +413 -0
- package/source/providers/PictProvider-Flow-Geometry.js +136 -2
- package/source/providers/PictProvider-Flow-Icons.js +335 -0
- package/source/providers/PictProvider-Flow-Layouts.js +214 -2
- package/source/providers/PictProvider-Flow-NodeTypes.js +30 -7
- package/source/providers/PictProvider-Flow-Noise.js +241 -0
- package/source/providers/PictProvider-Flow-PanelChrome.js +19 -0
- package/source/providers/PictProvider-Flow-Theme.js +755 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +95 -32
- package/source/services/PictService-Flow-PanelManager.js +188 -0
- package/source/services/PictService-Flow-SelectionManager.js +109 -0
- package/source/services/PictService-Flow-Tether.js +52 -25
- package/source/services/PictService-Flow-ViewportManager.js +176 -0
- package/source/views/PictView-Flow-FloatingToolbar.js +352 -0
- package/source/views/PictView-Flow-Node.js +668 -174
- package/source/views/PictView-Flow-PropertiesPanel.js +176 -1
- package/source/views/PictView-Flow-Toolbar.js +846 -379
- package/source/views/PictView-Flow.js +279 -671
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
2
|
+
|
|
3
|
+
class PictProviderFlowConnectorShapes extends libFableServiceProviderBase
|
|
4
|
+
{
|
|
5
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
6
|
+
{
|
|
7
|
+
super(pFable, pOptions, pServiceHash);
|
|
8
|
+
|
|
9
|
+
this.serviceType = 'PictProviderFlowConnectorShapes';
|
|
10
|
+
|
|
11
|
+
this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
|
|
12
|
+
|
|
13
|
+
// Default shape configurations — each keyed by a shape identifier
|
|
14
|
+
// _OriginalShapes stores a deep copy for reset when switching themes.
|
|
15
|
+
this._DefaultShapes =
|
|
16
|
+
{
|
|
17
|
+
'port':
|
|
18
|
+
{
|
|
19
|
+
ElementType: 'circle',
|
|
20
|
+
Attributes: { r: '5' },
|
|
21
|
+
ClassName: 'pict-flow-port'
|
|
22
|
+
},
|
|
23
|
+
'panel-indicator':
|
|
24
|
+
{
|
|
25
|
+
ElementType: 'rect',
|
|
26
|
+
Attributes: { rx: '2', ry: '2' },
|
|
27
|
+
ClassName: 'pict-flow-node-panel-indicator'
|
|
28
|
+
},
|
|
29
|
+
'connection-path':
|
|
30
|
+
{
|
|
31
|
+
ElementType: 'path',
|
|
32
|
+
Attributes: {},
|
|
33
|
+
ClassName: 'pict-flow-connection'
|
|
34
|
+
},
|
|
35
|
+
'connection-hitarea':
|
|
36
|
+
{
|
|
37
|
+
ElementType: 'path',
|
|
38
|
+
Attributes: {},
|
|
39
|
+
ClassName: 'pict-flow-connection-hitarea'
|
|
40
|
+
},
|
|
41
|
+
'connection-handle':
|
|
42
|
+
{
|
|
43
|
+
ElementType: 'circle',
|
|
44
|
+
Attributes: { r: '6' },
|
|
45
|
+
ClassName: 'pict-flow-connection-handle'
|
|
46
|
+
},
|
|
47
|
+
'connection-handle-midpoint':
|
|
48
|
+
{
|
|
49
|
+
ElementType: 'circle',
|
|
50
|
+
Attributes: { r: '6' },
|
|
51
|
+
ClassName: 'pict-flow-connection-handle-midpoint'
|
|
52
|
+
},
|
|
53
|
+
'drag-connection':
|
|
54
|
+
{
|
|
55
|
+
ElementType: 'path',
|
|
56
|
+
Attributes: {},
|
|
57
|
+
ClassName: 'pict-flow-drag-connection'
|
|
58
|
+
},
|
|
59
|
+
'tether-path':
|
|
60
|
+
{
|
|
61
|
+
ElementType: 'path',
|
|
62
|
+
Attributes: {},
|
|
63
|
+
ClassName: 'pict-flow-tether-line'
|
|
64
|
+
},
|
|
65
|
+
'tether-hitarea':
|
|
66
|
+
{
|
|
67
|
+
ElementType: 'path',
|
|
68
|
+
Attributes: {},
|
|
69
|
+
ClassName: 'pict-flow-tether-hitarea'
|
|
70
|
+
},
|
|
71
|
+
'tether-handle':
|
|
72
|
+
{
|
|
73
|
+
ElementType: 'circle',
|
|
74
|
+
Attributes: { r: '6' },
|
|
75
|
+
ClassName: 'pict-flow-tether-handle'
|
|
76
|
+
},
|
|
77
|
+
'tether-handle-midpoint':
|
|
78
|
+
{
|
|
79
|
+
ElementType: 'circle',
|
|
80
|
+
Attributes: { r: '6' },
|
|
81
|
+
ClassName: 'pict-flow-tether-handle-midpoint'
|
|
82
|
+
},
|
|
83
|
+
'arrowhead-connection':
|
|
84
|
+
{
|
|
85
|
+
MarkerWidth: '5',
|
|
86
|
+
MarkerHeight: '7',
|
|
87
|
+
RefX: '7.5',
|
|
88
|
+
RefY: '3.5',
|
|
89
|
+
Points: '0 0, 5 3.5, 0 7',
|
|
90
|
+
Fill: '#95a5a6'
|
|
91
|
+
},
|
|
92
|
+
'arrowhead-connection-selected':
|
|
93
|
+
{
|
|
94
|
+
MarkerWidth: '5',
|
|
95
|
+
MarkerHeight: '7',
|
|
96
|
+
RefX: '7.5',
|
|
97
|
+
RefY: '3.5',
|
|
98
|
+
Points: '0 0, 5 3.5, 0 7',
|
|
99
|
+
Fill: '#3498db'
|
|
100
|
+
},
|
|
101
|
+
'arrowhead-tether':
|
|
102
|
+
{
|
|
103
|
+
MarkerWidth: '4',
|
|
104
|
+
MarkerHeight: '6',
|
|
105
|
+
RefX: '6',
|
|
106
|
+
RefY: '3',
|
|
107
|
+
Points: '0 0, 4 3, 0 6',
|
|
108
|
+
Fill: '#95a5a6'
|
|
109
|
+
}
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
// Store a deep copy for resetting when switching themes
|
|
113
|
+
this._OriginalShapes = JSON.parse(JSON.stringify(this._DefaultShapes));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// ── Theme Override Methods ─────────────────────────────────────────────
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Apply theme-specific shape overrides.
|
|
120
|
+
* Merges the overrides into the active shape configs.
|
|
121
|
+
* @param {Object} pOverrides - Map of shape key to partial config
|
|
122
|
+
*/
|
|
123
|
+
applyThemeOverrides(pOverrides)
|
|
124
|
+
{
|
|
125
|
+
if (!pOverrides || typeof pOverrides !== 'object')
|
|
126
|
+
{
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
for (let tmpKey in pOverrides)
|
|
130
|
+
{
|
|
131
|
+
if (this._DefaultShapes.hasOwnProperty(tmpKey))
|
|
132
|
+
{
|
|
133
|
+
Object.assign(this._DefaultShapes[tmpKey], pOverrides[tmpKey]);
|
|
134
|
+
}
|
|
135
|
+
else
|
|
136
|
+
{
|
|
137
|
+
this._DefaultShapes[tmpKey] = pOverrides[tmpKey];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Reset all shape configs to their original defaults.
|
|
144
|
+
* Called before applying new theme overrides to prevent accumulation.
|
|
145
|
+
*/
|
|
146
|
+
resetToDefaults()
|
|
147
|
+
{
|
|
148
|
+
this._DefaultShapes = JSON.parse(JSON.stringify(this._OriginalShapes));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Get a shape configuration by key.
|
|
153
|
+
* @param {string} pShapeKey
|
|
154
|
+
* @returns {Object|null}
|
|
155
|
+
*/
|
|
156
|
+
getShapeConfig(pShapeKey)
|
|
157
|
+
{
|
|
158
|
+
if (this._DefaultShapes.hasOwnProperty(pShapeKey))
|
|
159
|
+
{
|
|
160
|
+
return this._DefaultShapes[pShapeKey];
|
|
161
|
+
}
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Override or add a shape configuration.
|
|
167
|
+
* Consumers can call this to customize connector shapes without subclassing.
|
|
168
|
+
* @param {string} pShapeKey
|
|
169
|
+
* @param {Object} pConfig
|
|
170
|
+
*/
|
|
171
|
+
setShapeConfig(pShapeKey, pConfig)
|
|
172
|
+
{
|
|
173
|
+
this._DefaultShapes[pShapeKey] = pConfig;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Get all registered shape keys.
|
|
178
|
+
* @returns {string[]}
|
|
179
|
+
*/
|
|
180
|
+
getShapeKeys()
|
|
181
|
+
{
|
|
182
|
+
return Object.keys(this._DefaultShapes);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ---- Factory Methods ----
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Create a port SVG element.
|
|
189
|
+
* @param {Object} pPortData - Port data with Hash, Direction
|
|
190
|
+
* @param {{x: number, y: number}} pPosition - Local position within the node group
|
|
191
|
+
* @param {string} pNodeHash - The owning node hash
|
|
192
|
+
* @returns {SVGCircleElement}
|
|
193
|
+
*/
|
|
194
|
+
createPortElement(pPortData, pPosition, pNodeHash)
|
|
195
|
+
{
|
|
196
|
+
let tmpConfig = this._DefaultShapes['port'];
|
|
197
|
+
let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
|
|
198
|
+
tmpElement.setAttribute('class', tmpConfig.ClassName + ' ' + pPortData.Direction);
|
|
199
|
+
tmpElement.setAttribute('cx', String(pPosition.x));
|
|
200
|
+
tmpElement.setAttribute('cy', String(pPosition.y));
|
|
201
|
+
// Apply config attributes (r, etc.)
|
|
202
|
+
for (let tmpKey in tmpConfig.Attributes)
|
|
203
|
+
{
|
|
204
|
+
tmpElement.setAttribute(tmpKey, tmpConfig.Attributes[tmpKey]);
|
|
205
|
+
}
|
|
206
|
+
tmpElement.setAttribute('data-port-hash', pPortData.Hash);
|
|
207
|
+
tmpElement.setAttribute('data-node-hash', pNodeHash);
|
|
208
|
+
tmpElement.setAttribute('data-port-direction', pPortData.Direction);
|
|
209
|
+
tmpElement.setAttribute('data-element-type', 'port');
|
|
210
|
+
return tmpElement;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Create a panel indicator SVG element.
|
|
215
|
+
* @param {string} pNodeHash
|
|
216
|
+
* @param {number} pX
|
|
217
|
+
* @param {number} pY
|
|
218
|
+
* @param {number} pWidth
|
|
219
|
+
* @param {number} pHeight
|
|
220
|
+
* @returns {SVGRectElement}
|
|
221
|
+
*/
|
|
222
|
+
createPanelIndicatorElement(pNodeHash, pX, pY, pWidth, pHeight)
|
|
223
|
+
{
|
|
224
|
+
let tmpConfig = this._DefaultShapes['panel-indicator'];
|
|
225
|
+
let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
|
|
226
|
+
tmpElement.setAttribute('class', tmpConfig.ClassName);
|
|
227
|
+
tmpElement.setAttribute('x', String(pX));
|
|
228
|
+
tmpElement.setAttribute('y', String(pY));
|
|
229
|
+
tmpElement.setAttribute('width', String(pWidth));
|
|
230
|
+
tmpElement.setAttribute('height', String(pHeight));
|
|
231
|
+
// Apply config attributes (rx, ry, etc.)
|
|
232
|
+
for (let tmpKey in tmpConfig.Attributes)
|
|
233
|
+
{
|
|
234
|
+
tmpElement.setAttribute(tmpKey, tmpConfig.Attributes[tmpKey]);
|
|
235
|
+
}
|
|
236
|
+
tmpElement.setAttribute('data-node-hash', pNodeHash);
|
|
237
|
+
tmpElement.setAttribute('data-element-type', 'panel-indicator');
|
|
238
|
+
return tmpElement;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Create a visible connection path SVG element.
|
|
243
|
+
* @param {string} pPath - The SVG path d-string
|
|
244
|
+
* @param {string} pConnectionHash
|
|
245
|
+
* @param {boolean} pIsSelected
|
|
246
|
+
* @param {string} pViewIdentifier
|
|
247
|
+
* @returns {SVGPathElement}
|
|
248
|
+
*/
|
|
249
|
+
createConnectionPathElement(pPath, pConnectionHash, pIsSelected, pViewIdentifier)
|
|
250
|
+
{
|
|
251
|
+
let tmpConfig = this._DefaultShapes['connection-path'];
|
|
252
|
+
let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
|
|
253
|
+
tmpElement.setAttribute('class', tmpConfig.ClassName + (pIsSelected ? ' selected' : ''));
|
|
254
|
+
tmpElement.setAttribute('d', pPath);
|
|
255
|
+
tmpElement.setAttribute('data-connection-hash', pConnectionHash);
|
|
256
|
+
tmpElement.setAttribute('data-element-type', 'connection');
|
|
257
|
+
|
|
258
|
+
// Arrow marker
|
|
259
|
+
let tmpMarkerConfig = pIsSelected
|
|
260
|
+
? this._DefaultShapes['arrowhead-connection-selected']
|
|
261
|
+
: this._DefaultShapes['arrowhead-connection'];
|
|
262
|
+
// The marker id follows the naming convention used in generateMarkerDefs
|
|
263
|
+
let tmpMarkerId = pIsSelected
|
|
264
|
+
? ('flow-arrowhead-selected-' + pViewIdentifier)
|
|
265
|
+
: ('flow-arrowhead-' + pViewIdentifier);
|
|
266
|
+
tmpElement.setAttribute('marker-end', 'url(#' + tmpMarkerId + ')');
|
|
267
|
+
|
|
268
|
+
return tmpElement;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Create a connection hit area SVG element (wider invisible path for click targeting).
|
|
273
|
+
* @param {string} pPath - The SVG path d-string
|
|
274
|
+
* @param {string} pConnectionHash
|
|
275
|
+
* @returns {SVGPathElement}
|
|
276
|
+
*/
|
|
277
|
+
createConnectionHitAreaElement(pPath, pConnectionHash)
|
|
278
|
+
{
|
|
279
|
+
let tmpConfig = this._DefaultShapes['connection-hitarea'];
|
|
280
|
+
let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
|
|
281
|
+
tmpElement.setAttribute('class', tmpConfig.ClassName);
|
|
282
|
+
tmpElement.setAttribute('d', pPath);
|
|
283
|
+
tmpElement.setAttribute('data-connection-hash', pConnectionHash);
|
|
284
|
+
tmpElement.setAttribute('data-element-type', 'connection-hitarea');
|
|
285
|
+
return tmpElement;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Create a drag handle circle element.
|
|
290
|
+
* Works for both connection handles and tether handles.
|
|
291
|
+
* @param {string} pOwnerHash - Connection hash or panel hash
|
|
292
|
+
* @param {string} pHandleType - e.g. 'ortho-corner1', 'bezier-midpoint'
|
|
293
|
+
* @param {number} pX
|
|
294
|
+
* @param {number} pY
|
|
295
|
+
* @param {string} pShapeKey - 'connection-handle', 'connection-handle-midpoint', 'tether-handle', 'tether-handle-midpoint'
|
|
296
|
+
* @returns {SVGCircleElement}
|
|
297
|
+
*/
|
|
298
|
+
createHandleElement(pOwnerHash, pHandleType, pX, pY, pShapeKey)
|
|
299
|
+
{
|
|
300
|
+
let tmpConfig = this._DefaultShapes[pShapeKey] || this._DefaultShapes['connection-handle'];
|
|
301
|
+
let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
|
|
302
|
+
tmpElement.setAttribute('class', tmpConfig.ClassName);
|
|
303
|
+
tmpElement.setAttribute('cx', String(pX));
|
|
304
|
+
tmpElement.setAttribute('cy', String(pY));
|
|
305
|
+
// Apply config attributes (r, etc.)
|
|
306
|
+
for (let tmpKey in tmpConfig.Attributes)
|
|
307
|
+
{
|
|
308
|
+
tmpElement.setAttribute(tmpKey, tmpConfig.Attributes[tmpKey]);
|
|
309
|
+
}
|
|
310
|
+
tmpElement.setAttribute('data-handle-type', pHandleType);
|
|
311
|
+
return tmpElement;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Create a temporary drag connection path element.
|
|
316
|
+
* @param {string} pPath - The SVG path d-string
|
|
317
|
+
* @returns {SVGPathElement}
|
|
318
|
+
*/
|
|
319
|
+
createDragConnectionElement(pPath)
|
|
320
|
+
{
|
|
321
|
+
let tmpConfig = this._DefaultShapes['drag-connection'];
|
|
322
|
+
let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
|
|
323
|
+
tmpElement.setAttribute('class', tmpConfig.ClassName);
|
|
324
|
+
tmpElement.setAttribute('d', pPath);
|
|
325
|
+
return tmpElement;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Create a visible tether path SVG element.
|
|
330
|
+
* @param {string} pPath - The SVG path d-string
|
|
331
|
+
* @param {string} pPanelHash
|
|
332
|
+
* @param {boolean} pIsSelected
|
|
333
|
+
* @param {string} pViewIdentifier
|
|
334
|
+
* @returns {SVGPathElement}
|
|
335
|
+
*/
|
|
336
|
+
createTetherPathElement(pPath, pPanelHash, pIsSelected, pViewIdentifier)
|
|
337
|
+
{
|
|
338
|
+
let tmpConfig = this._DefaultShapes['tether-path'];
|
|
339
|
+
let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
|
|
340
|
+
tmpElement.setAttribute('class', tmpConfig.ClassName + (pIsSelected ? ' selected' : ''));
|
|
341
|
+
tmpElement.setAttribute('d', pPath);
|
|
342
|
+
tmpElement.setAttribute('marker-end', 'url(#flow-tether-arrowhead-' + pViewIdentifier + ')');
|
|
343
|
+
tmpElement.setAttribute('data-element-type', 'tether');
|
|
344
|
+
tmpElement.setAttribute('data-panel-hash', pPanelHash);
|
|
345
|
+
return tmpElement;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Create a tether hit area SVG element.
|
|
350
|
+
* @param {string} pPath - The SVG path d-string
|
|
351
|
+
* @param {string} pPanelHash
|
|
352
|
+
* @returns {SVGPathElement}
|
|
353
|
+
*/
|
|
354
|
+
createTetherHitAreaElement(pPath, pPanelHash)
|
|
355
|
+
{
|
|
356
|
+
let tmpConfig = this._DefaultShapes['tether-hitarea'];
|
|
357
|
+
let tmpElement = this._FlowView._SVGHelperProvider.createSVGElement(tmpConfig.ElementType);
|
|
358
|
+
tmpElement.setAttribute('class', tmpConfig.ClassName);
|
|
359
|
+
tmpElement.setAttribute('d', pPath);
|
|
360
|
+
tmpElement.setAttribute('data-element-type', 'tether-hitarea');
|
|
361
|
+
tmpElement.setAttribute('data-panel-hash', pPanelHash);
|
|
362
|
+
return tmpElement;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/**
|
|
366
|
+
* Generate SVG marker definition markup for all arrowhead types.
|
|
367
|
+
* Returns raw SVG markup to be injected into the <defs> section.
|
|
368
|
+
* @param {string} pViewIdentifier
|
|
369
|
+
* @returns {string}
|
|
370
|
+
*/
|
|
371
|
+
generateMarkerDefs(pViewIdentifier)
|
|
372
|
+
{
|
|
373
|
+
let tmpConnectionMarker = this._DefaultShapes['arrowhead-connection'];
|
|
374
|
+
let tmpSelectedMarker = this._DefaultShapes['arrowhead-connection-selected'];
|
|
375
|
+
let tmpTetherMarker = this._DefaultShapes['arrowhead-tether'];
|
|
376
|
+
|
|
377
|
+
let tmpMarkup = '';
|
|
378
|
+
|
|
379
|
+
// Normal connection arrowhead
|
|
380
|
+
tmpMarkup += '<marker id="flow-arrowhead-' + pViewIdentifier + '"'
|
|
381
|
+
+ ' markerWidth="' + tmpConnectionMarker.MarkerWidth + '"'
|
|
382
|
+
+ ' markerHeight="' + tmpConnectionMarker.MarkerHeight + '"'
|
|
383
|
+
+ ' refX="' + tmpConnectionMarker.RefX + '"'
|
|
384
|
+
+ ' refY="' + tmpConnectionMarker.RefY + '"'
|
|
385
|
+
+ ' orient="auto" markerUnits="strokeWidth">'
|
|
386
|
+
+ '<polygon points="' + tmpConnectionMarker.Points + '" fill="' + tmpConnectionMarker.Fill + '" />'
|
|
387
|
+
+ '</marker>';
|
|
388
|
+
|
|
389
|
+
// Selected connection arrowhead
|
|
390
|
+
tmpMarkup += '<marker id="flow-arrowhead-selected-' + pViewIdentifier + '"'
|
|
391
|
+
+ ' markerWidth="' + tmpSelectedMarker.MarkerWidth + '"'
|
|
392
|
+
+ ' markerHeight="' + tmpSelectedMarker.MarkerHeight + '"'
|
|
393
|
+
+ ' refX="' + tmpSelectedMarker.RefX + '"'
|
|
394
|
+
+ ' refY="' + tmpSelectedMarker.RefY + '"'
|
|
395
|
+
+ ' orient="auto" markerUnits="strokeWidth">'
|
|
396
|
+
+ '<polygon points="' + tmpSelectedMarker.Points + '" fill="' + tmpSelectedMarker.Fill + '" />'
|
|
397
|
+
+ '</marker>';
|
|
398
|
+
|
|
399
|
+
// Tether arrowhead
|
|
400
|
+
tmpMarkup += '<marker id="flow-tether-arrowhead-' + pViewIdentifier + '"'
|
|
401
|
+
+ ' markerWidth="' + tmpTetherMarker.MarkerWidth + '"'
|
|
402
|
+
+ ' markerHeight="' + tmpTetherMarker.MarkerHeight + '"'
|
|
403
|
+
+ ' refX="' + tmpTetherMarker.RefX + '"'
|
|
404
|
+
+ ' refY="' + tmpTetherMarker.RefY + '"'
|
|
405
|
+
+ ' orient="auto" markerUnits="strokeWidth">'
|
|
406
|
+
+ '<polygon points="' + tmpTetherMarker.Points + '" fill="' + tmpTetherMarker.Fill + '" />'
|
|
407
|
+
+ '</marker>';
|
|
408
|
+
|
|
409
|
+
return tmpMarkup;
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
module.exports = PictProviderFlowConnectorShapes;
|
|
@@ -6,6 +6,16 @@ const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
|
6
6
|
* Shared geometry utilities for the flow diagram.
|
|
7
7
|
* Provides direction vectors and edge center calculations used by
|
|
8
8
|
* connections, tethers, and other flow components.
|
|
9
|
+
*
|
|
10
|
+
* Port Side values (12 positions):
|
|
11
|
+
*
|
|
12
|
+
* Top edge: 'top-left' 'top' 'top-right'
|
|
13
|
+
* Left edge: 'left-top' 'left' 'left-bottom'
|
|
14
|
+
* Right edge: 'right-top' 'right' 'right-bottom'
|
|
15
|
+
* Bottom edge: 'bottom-left' 'bottom' 'bottom-right'
|
|
16
|
+
*
|
|
17
|
+
* The old 4-value sides ('left', 'right', 'top', 'bottom') map to
|
|
18
|
+
* the middle position on each edge for backward compatibility.
|
|
9
19
|
*/
|
|
10
20
|
class PictProviderFlowGeometry extends libFableServiceProviderBase
|
|
11
21
|
{
|
|
@@ -16,15 +26,55 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
|
|
|
16
26
|
this.serviceType = 'PictProviderFlowGeometry';
|
|
17
27
|
}
|
|
18
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Extract the edge name from a Side value.
|
|
31
|
+
*
|
|
32
|
+
* Maps all 12 positions (and the 4 legacy values) back to
|
|
33
|
+
* the edge they sit on: 'left', 'right', 'top', or 'bottom'.
|
|
34
|
+
*
|
|
35
|
+
* @param {string} pSide - Any valid Side value
|
|
36
|
+
* @returns {string} The edge: 'left', 'right', 'top', or 'bottom'
|
|
37
|
+
*/
|
|
38
|
+
getEdgeFromSide(pSide)
|
|
39
|
+
{
|
|
40
|
+
switch (pSide)
|
|
41
|
+
{
|
|
42
|
+
case 'left-top':
|
|
43
|
+
case 'left':
|
|
44
|
+
case 'left-bottom':
|
|
45
|
+
return 'left';
|
|
46
|
+
|
|
47
|
+
case 'right-top':
|
|
48
|
+
case 'right':
|
|
49
|
+
case 'right-bottom':
|
|
50
|
+
return 'right';
|
|
51
|
+
|
|
52
|
+
case 'top-left':
|
|
53
|
+
case 'top':
|
|
54
|
+
case 'top-right':
|
|
55
|
+
return 'top';
|
|
56
|
+
|
|
57
|
+
case 'bottom-left':
|
|
58
|
+
case 'bottom':
|
|
59
|
+
case 'bottom-right':
|
|
60
|
+
return 'bottom';
|
|
61
|
+
|
|
62
|
+
default:
|
|
63
|
+
return 'right';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
19
67
|
/**
|
|
20
68
|
* Get the outward unit direction vector for a given side.
|
|
21
69
|
*
|
|
22
|
-
*
|
|
70
|
+
* All positions on the same edge share the same direction vector.
|
|
71
|
+
*
|
|
72
|
+
* @param {string} pSide - Any valid Side value (12 positions or 4 legacy)
|
|
23
73
|
* @returns {{dx: number, dy: number}}
|
|
24
74
|
*/
|
|
25
75
|
sideDirection(pSide)
|
|
26
76
|
{
|
|
27
|
-
switch (pSide)
|
|
77
|
+
switch (this.getEdgeFromSide(pSide))
|
|
28
78
|
{
|
|
29
79
|
case 'left': return { dx: -1, dy: 0 };
|
|
30
80
|
case 'right': return { dx: 1, dy: 0 };
|
|
@@ -59,6 +109,90 @@ class PictProviderFlowGeometry extends libFableServiceProviderBase
|
|
|
59
109
|
return { x: pRectData.X + pRectData.Width, y: pRectData.Y + pRectData.Height / 2 };
|
|
60
110
|
}
|
|
61
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Calculate a port's local position relative to node origin.
|
|
115
|
+
*
|
|
116
|
+
* Supports 12 positions (3 zones per edge). For left/right edges,
|
|
117
|
+
* the body area below the title bar is divided into three vertical zones
|
|
118
|
+
* (start/middle/end). For top/bottom edges, the full width is divided
|
|
119
|
+
* into three horizontal zones.
|
|
120
|
+
*
|
|
121
|
+
* Multiple ports sharing the same Side value distribute evenly within
|
|
122
|
+
* their zone.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} pSide - Side value (any of 12 positions or 4 legacy)
|
|
125
|
+
* @param {number} pIndex - Index of this port within its Side group
|
|
126
|
+
* @param {number} pTotal - Total ports with this Side value
|
|
127
|
+
* @param {number} pWidth - Node width
|
|
128
|
+
* @param {number} pHeight - Node height
|
|
129
|
+
* @param {number} pTitleBarHeight - Height of the node title bar
|
|
130
|
+
* @returns {{x: number, y: number}}
|
|
131
|
+
*/
|
|
132
|
+
getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, pTitleBarHeight)
|
|
133
|
+
{
|
|
134
|
+
let tmpEdge = this.getEdgeFromSide(pSide);
|
|
135
|
+
let tmpZone = this._getZoneFromSide(pSide);
|
|
136
|
+
|
|
137
|
+
if (tmpEdge === 'left' || tmpEdge === 'right')
|
|
138
|
+
{
|
|
139
|
+
let tmpX = (tmpEdge === 'left') ? 0 : pWidth;
|
|
140
|
+
let tmpBodyHeight = pHeight - pTitleBarHeight;
|
|
141
|
+
let tmpZoneStart = pTitleBarHeight + tmpBodyHeight * tmpZone.start;
|
|
142
|
+
let tmpZoneHeight = tmpBodyHeight * (tmpZone.end - tmpZone.start);
|
|
143
|
+
let tmpSpacing = tmpZoneHeight / (pTotal + 1);
|
|
144
|
+
let tmpY = tmpZoneStart + tmpSpacing * (pIndex + 1);
|
|
145
|
+
return { x: tmpX, y: tmpY };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// top or bottom
|
|
149
|
+
let tmpY = (tmpEdge === 'top') ? 0 : pHeight;
|
|
150
|
+
let tmpZoneStart = pWidth * tmpZone.start;
|
|
151
|
+
let tmpZoneWidth = pWidth * (tmpZone.end - tmpZone.start);
|
|
152
|
+
let tmpSpacing = tmpZoneWidth / (pTotal + 1);
|
|
153
|
+
let tmpX = tmpZoneStart + tmpSpacing * (pIndex + 1);
|
|
154
|
+
return { x: tmpX, y: tmpY };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get the zone fraction (start, end) for a Side value.
|
|
159
|
+
*
|
|
160
|
+
* Each edge is divided into three zones of equal size:
|
|
161
|
+
* start: 0.0 — 0.333
|
|
162
|
+
* middle: 0.333 — 0.667
|
|
163
|
+
* end: 0.667 — 1.0
|
|
164
|
+
*
|
|
165
|
+
* @param {string} pSide
|
|
166
|
+
* @returns {{start: number, end: number}}
|
|
167
|
+
*/
|
|
168
|
+
_getZoneFromSide(pSide)
|
|
169
|
+
{
|
|
170
|
+
switch (pSide)
|
|
171
|
+
{
|
|
172
|
+
// Left edge: top, middle, bottom
|
|
173
|
+
case 'left-top': return { start: 0.0, end: 0.333 };
|
|
174
|
+
case 'left': return { start: 0.333, end: 0.667 };
|
|
175
|
+
case 'left-bottom': return { start: 0.667, end: 1.0 };
|
|
176
|
+
|
|
177
|
+
// Right edge: top, middle, bottom
|
|
178
|
+
case 'right-top': return { start: 0.0, end: 0.333 };
|
|
179
|
+
case 'right': return { start: 0.333, end: 0.667 };
|
|
180
|
+
case 'right-bottom': return { start: 0.667, end: 1.0 };
|
|
181
|
+
|
|
182
|
+
// Top edge: left, middle, right
|
|
183
|
+
case 'top-left': return { start: 0.0, end: 0.333 };
|
|
184
|
+
case 'top': return { start: 0.333, end: 0.667 };
|
|
185
|
+
case 'top-right': return { start: 0.667, end: 1.0 };
|
|
186
|
+
|
|
187
|
+
// Bottom edge: left, middle, right
|
|
188
|
+
case 'bottom-left': return { start: 0.0, end: 0.333 };
|
|
189
|
+
case 'bottom': return { start: 0.333, end: 0.667 };
|
|
190
|
+
case 'bottom-right': return { start: 0.667, end: 1.0 };
|
|
191
|
+
|
|
192
|
+
// Fallback: full range (legacy behavior)
|
|
193
|
+
default: return { start: 0.0, end: 1.0 };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
62
196
|
}
|
|
63
197
|
|
|
64
198
|
module.exports = PictProviderFlowGeometry;
|