pict-section-flow 0.0.1 → 0.0.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/docs/README.md +19 -0
- package/{example_application → example_applications/simple_cards}/html/index.html +2 -2
- package/example_applications/simple_cards/package.json +43 -0
- package/example_applications/simple_cards/source/Pict-Application-FlowExample.js +434 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Each.js +36 -0
- package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +54 -0
- package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +48 -0
- package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +35 -0
- package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +47 -0
- package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +53 -0
- package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +95 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +37 -0
- package/example_applications/simple_cards/source/views/PictView-FlowExample-FileWriteInfo.js +59 -0
- package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-Layout.js +5 -1
- package/example_applications/simple_cards/source/views/PictView-FlowExample-MainWorkspace.js +312 -0
- package/package.json +6 -6
- package/source/Pict-Section-Flow.js +19 -0
- package/source/PictFlowCard.js +207 -0
- package/source/PictFlowCardPropertiesPanel.js +105 -0
- package/source/panels/FlowCardPropertiesPanel-Form.js +174 -0
- package/source/panels/FlowCardPropertiesPanel-Markdown.js +148 -0
- package/source/panels/FlowCardPropertiesPanel-Template.js +88 -0
- package/source/panels/FlowCardPropertiesPanel-View.js +114 -0
- package/source/providers/PictProvider-Flow-EventHandler.js +19 -8
- package/source/providers/PictProvider-Flow-Geometry.js +64 -0
- package/source/providers/PictProvider-Flow-Layouts.js +284 -0
- package/source/providers/PictProvider-Flow-NodeTypes.js +70 -0
- package/source/providers/PictProvider-Flow-PanelChrome.js +72 -0
- package/source/providers/PictProvider-Flow-SVGHelpers.js +30 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +324 -66
- package/source/services/PictService-Flow-InteractionManager.js +399 -75
- package/source/services/PictService-Flow-Layout.js +159 -0
- package/source/services/PictService-Flow-PathGenerator.js +199 -0
- package/source/services/PictService-Flow-Tether.js +544 -0
- package/source/views/PictView-Flow-Node.js +95 -18
- package/source/views/PictView-Flow-PropertiesPanel.js +435 -0
- package/source/views/PictView-Flow-Toolbar.js +491 -5
- package/source/views/PictView-Flow.js +830 -8
- package/example_application/package.json +0 -41
- package/example_application/source/Pict-Application-FlowExample.js +0 -241
- package/example_application/source/views/PictView-FlowExample-MainWorkspace.js +0 -191
- /package/{example_application → example_applications/simple_cards}/css/flowexample.css +0 -0
- /package/{example_application → example_applications/simple_cards}/source/Pict-Application-FlowExample-Configuration.json +0 -0
- /package/{example_application → example_applications/simple_cards}/source/providers/PictRouter-FlowExample-Configuration.json +0 -0
- /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-About.js +0 -0
- /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-BottomBar.js +0 -0
- /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-Documentation.js +0 -0
- /package/{example_application → example_applications/simple_cards}/source/views/PictView-FlowExample-TopBar.js +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PictProvider-Flow-PanelChrome
|
|
5
|
+
*
|
|
6
|
+
* Template-based provider for creating the panel chrome (the foreignObject
|
|
7
|
+
* wrapper, title bar, close button, and body container) for properties panels.
|
|
8
|
+
*
|
|
9
|
+
* Replaces the raw DOM API approach with a configuration template
|
|
10
|
+
* (Flow-PanelChrome-Template) registered in the FlowView's template set.
|
|
11
|
+
*/
|
|
12
|
+
class PictProviderFlowPanelChrome extends libFableServiceProviderBase
|
|
13
|
+
{
|
|
14
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
15
|
+
{
|
|
16
|
+
super(pFable, pOptions, pServiceHash);
|
|
17
|
+
|
|
18
|
+
this.serviceType = 'PictProviderFlowPanelChrome';
|
|
19
|
+
|
|
20
|
+
this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Create a foreignObject containing the panel chrome and empty body container.
|
|
25
|
+
*
|
|
26
|
+
* Uses the Flow-PanelChrome-Template registered in the FlowView configuration
|
|
27
|
+
* to render the inner HTML (title bar, close button, body container), then
|
|
28
|
+
* attaches event isolation listeners so pointer and wheel events inside the
|
|
29
|
+
* panel body do not propagate to the SVG interaction layer.
|
|
30
|
+
*
|
|
31
|
+
* @param {Object} pPanelData - Panel data from OpenPanels (Hash, NodeHash, X, Y, Width, Height, Title)
|
|
32
|
+
* @param {SVGGElement} pPanelsLayer - The SVG <g> for panel foreignObjects
|
|
33
|
+
* @returns {HTMLDivElement} The panel body container element for content rendering
|
|
34
|
+
*/
|
|
35
|
+
createPanelForeignObject(pPanelData, pPanelsLayer)
|
|
36
|
+
{
|
|
37
|
+
let tmpSVGHelper = this._FlowView._SVGHelperProvider;
|
|
38
|
+
|
|
39
|
+
// Create the SVG foreignObject wrapper
|
|
40
|
+
let tmpFO = tmpSVGHelper.createSVGElement('foreignObject');
|
|
41
|
+
tmpFO.setAttribute('class', 'pict-flow-panel-foreign-object');
|
|
42
|
+
tmpFO.setAttribute('data-panel-hash', pPanelData.Hash);
|
|
43
|
+
tmpFO.setAttribute('data-node-hash', pPanelData.NodeHash);
|
|
44
|
+
tmpFO.setAttribute('x', String(pPanelData.X));
|
|
45
|
+
tmpFO.setAttribute('y', String(pPanelData.Y));
|
|
46
|
+
tmpFO.setAttribute('width', String(pPanelData.Width));
|
|
47
|
+
tmpFO.setAttribute('height', String(pPanelData.Height));
|
|
48
|
+
|
|
49
|
+
// Render the panel chrome from the configuration template
|
|
50
|
+
let tmpPict = this._FlowView.pict || this._FlowView.fable;
|
|
51
|
+
let tmpTitle = pPanelData.Title || 'Properties';
|
|
52
|
+
let tmpChromeHTML = tmpPict.parseTemplateByHash('Flow-PanelChrome-Template',
|
|
53
|
+
{ Hash: pPanelData.Hash, Title: tmpTitle });
|
|
54
|
+
|
|
55
|
+
tmpFO.innerHTML = tmpChromeHTML;
|
|
56
|
+
|
|
57
|
+
// Attach event isolation to the panel body so pointer/wheel events
|
|
58
|
+
// inside the panel content do not trigger SVG interactions
|
|
59
|
+
let tmpBody = tmpFO.querySelector('.pict-flow-panel-body');
|
|
60
|
+
if (tmpBody)
|
|
61
|
+
{
|
|
62
|
+
tmpBody.addEventListener('pointerdown', (pEvent) => { pEvent.stopPropagation(); });
|
|
63
|
+
tmpBody.addEventListener('wheel', (pEvent) => { pEvent.stopPropagation(); });
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
pPanelsLayer.appendChild(tmpFO);
|
|
67
|
+
|
|
68
|
+
return tmpBody;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = PictProviderFlowPanelChrome;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PictProvider-Flow-SVGHelpers
|
|
5
|
+
*
|
|
6
|
+
* Shared SVG element creation utility used by all flow components
|
|
7
|
+
* that need to create SVG namespace elements (nodes, connections, tethers).
|
|
8
|
+
*/
|
|
9
|
+
class PictProviderFlowSVGHelpers extends libFableServiceProviderBase
|
|
10
|
+
{
|
|
11
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
12
|
+
{
|
|
13
|
+
super(pFable, pOptions, pServiceHash);
|
|
14
|
+
|
|
15
|
+
this.serviceType = 'PictProviderFlowSVGHelpers';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Create an SVG namespace element.
|
|
20
|
+
*
|
|
21
|
+
* @param {string} pTagName - The SVG element tag name (e.g., 'path', 'circle', 'g', 'rect', 'text')
|
|
22
|
+
* @returns {SVGElement}
|
|
23
|
+
*/
|
|
24
|
+
createSVGElement(pTagName)
|
|
25
|
+
{
|
|
26
|
+
return document.createElementNS('http://www.w3.org/2000/svg', pTagName);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = PictProviderFlowSVGHelpers;
|
|
@@ -12,17 +12,7 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
*
|
|
16
|
-
* @param {string} pTagName
|
|
17
|
-
* @returns {SVGElement}
|
|
18
|
-
*/
|
|
19
|
-
_createSVGElement(pTagName)
|
|
20
|
-
{
|
|
21
|
-
return document.createElementNS('http://www.w3.org/2000/svg', pTagName);
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* Render a connection as an SVG path with hit area
|
|
15
|
+
* Render a connection as an SVG path with hit area and optional handles.
|
|
26
16
|
* @param {Object} pConnection - The connection data
|
|
27
17
|
* @param {SVGGElement} pConnectionsLayer - The SVG group to append to
|
|
28
18
|
* @param {boolean} pIsSelected - Whether this connection is selected
|
|
@@ -36,11 +26,34 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
36
26
|
|
|
37
27
|
if (!tmpSourcePos || !tmpTargetPos) return;
|
|
38
28
|
|
|
39
|
-
let
|
|
29
|
+
let tmpData = pConnection.Data || {};
|
|
30
|
+
let tmpLineMode = tmpData.LineMode || 'bezier';
|
|
31
|
+
let tmpPath;
|
|
32
|
+
|
|
33
|
+
if (tmpLineMode === 'orthogonal')
|
|
34
|
+
{
|
|
35
|
+
let tmpCorners = null;
|
|
36
|
+
if (tmpData.HandleCustomized && tmpData.OrthoCorner1X != null)
|
|
37
|
+
{
|
|
38
|
+
tmpCorners =
|
|
39
|
+
{
|
|
40
|
+
corner1: { x: tmpData.OrthoCorner1X, y: tmpData.OrthoCorner1Y },
|
|
41
|
+
corner2: { x: tmpData.OrthoCorner2X, y: tmpData.OrthoCorner2Y }
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
tmpPath = this._generateOrthogonalPath(tmpSourcePos, tmpTargetPos, tmpCorners, tmpData.OrthoMidOffset || 0);
|
|
45
|
+
}
|
|
46
|
+
else
|
|
47
|
+
{
|
|
48
|
+
let tmpHandleX = (tmpData.HandleCustomized && tmpData.BezierHandleX != null) ? tmpData.BezierHandleX : null;
|
|
49
|
+
let tmpHandleY = (tmpData.HandleCustomized && tmpData.BezierHandleY != null) ? tmpData.BezierHandleY : null;
|
|
50
|
+
tmpPath = this._generateBezierPathWithHandle(tmpSourcePos, tmpTargetPos, tmpHandleX, tmpHandleY);
|
|
51
|
+
}
|
|
52
|
+
|
|
40
53
|
let tmpViewIdentifier = this._FlowView.options.ViewIdentifier;
|
|
41
54
|
|
|
42
55
|
// Hit area (wider invisible path for easier selection)
|
|
43
|
-
let tmpHitArea = this.
|
|
56
|
+
let tmpHitArea = this._FlowView._SVGHelperProvider.createSVGElement('path');
|
|
44
57
|
tmpHitArea.setAttribute('class', 'pict-flow-connection-hitarea');
|
|
45
58
|
tmpHitArea.setAttribute('d', tmpPath);
|
|
46
59
|
tmpHitArea.setAttribute('data-connection-hash', pConnection.Hash);
|
|
@@ -48,7 +61,7 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
48
61
|
pConnectionsLayer.appendChild(tmpHitArea);
|
|
49
62
|
|
|
50
63
|
// Visible connection path
|
|
51
|
-
let tmpPathElement = this.
|
|
64
|
+
let tmpPathElement = this._FlowView._SVGHelperProvider.createSVGElement('path');
|
|
52
65
|
tmpPathElement.setAttribute('class', `pict-flow-connection ${pIsSelected ? 'selected' : ''}`);
|
|
53
66
|
tmpPathElement.setAttribute('d', tmpPath);
|
|
54
67
|
tmpPathElement.setAttribute('data-connection-hash', pConnection.Hash);
|
|
@@ -65,77 +78,47 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
65
78
|
}
|
|
66
79
|
|
|
67
80
|
pConnectionsLayer.appendChild(tmpPathElement);
|
|
68
|
-
}
|
|
69
81
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
*
|
|
73
|
-
* @param {string} pSide - 'left', 'right', 'top', or 'bottom'
|
|
74
|
-
* @returns {{dx: number, dy: number}}
|
|
75
|
-
*/
|
|
76
|
-
_sideDirection(pSide)
|
|
77
|
-
{
|
|
78
|
-
switch (pSide)
|
|
82
|
+
// Render drag handles when selected
|
|
83
|
+
if (pIsSelected)
|
|
79
84
|
{
|
|
80
|
-
|
|
81
|
-
case 'right': return { dx: 1, dy: 0 };
|
|
82
|
-
case 'top': return { dx: 0, dy: -1 };
|
|
83
|
-
case 'bottom': return { dx: 0, dy: 1 };
|
|
84
|
-
default: return { dx: 1, dy: 0 };
|
|
85
|
+
this._renderHandles(pConnection, pConnectionsLayer, tmpSourcePos, tmpTargetPos);
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
88
89
|
/**
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
* The path is composed of three segments:
|
|
92
|
-
* 1. A short straight "departure" segment leaving the source port
|
|
93
|
-
* in its outward direction.
|
|
94
|
-
* 2. A cubic bezier curve connecting the departure point to an
|
|
95
|
-
* "approach" point near the target port.
|
|
96
|
-
* 3. A short straight "approach" segment arriving at the target
|
|
97
|
-
* port aligned with its inward direction.
|
|
90
|
+
* Compute the departure and approach points plus control points
|
|
91
|
+
* for a direction-aware bezier between two ports.
|
|
98
92
|
*
|
|
99
|
-
*
|
|
100
|
-
*
|
|
101
|
-
* port) regardless of the overall curve shape.
|
|
93
|
+
* This extracts the intermediate geometry from _generateDirectionalPath
|
|
94
|
+
* so it can be reused by _getAutoMidpoint and _generateBezierPathWithHandle.
|
|
102
95
|
*
|
|
103
|
-
* @param {{x: number, y: number, side: string}} pStart
|
|
104
|
-
* @param {{x: number, y: number, side: string}} pEnd
|
|
105
|
-
* @returns {
|
|
96
|
+
* @param {{x: number, y: number, side: string}} pStart
|
|
97
|
+
* @param {{x: number, y: number, side: string}} pEnd
|
|
98
|
+
* @returns {{departX, departY, approachX, approachY, cp1X, cp1Y, cp2X, cp2Y, startDir, endDir}}
|
|
106
99
|
*/
|
|
107
|
-
|
|
100
|
+
_computeDirectionalGeometry(pStart, pEnd)
|
|
108
101
|
{
|
|
109
|
-
let tmpStartDir = this.
|
|
110
|
-
let tmpEndDir = this.
|
|
102
|
+
let tmpStartDir = this._FlowView._GeometryProvider.sideDirection(pStart.side || 'right');
|
|
103
|
+
let tmpEndDir = this._FlowView._GeometryProvider.sideDirection(pEnd.side || 'left');
|
|
111
104
|
|
|
112
|
-
// Length of the straight segments leaving/entering each port.
|
|
113
|
-
// The arrowhead is ~20px (10 marker * 2 stroke-width), so
|
|
114
|
-
// the approach segment needs to be at least that long to look
|
|
115
|
-
// clean. We use 20px as a minimum.
|
|
116
105
|
let tmpStraightLen = 20;
|
|
117
106
|
|
|
118
|
-
// Departure point: offset from source port along its outward direction
|
|
119
107
|
let tmpDepartX = pStart.x + tmpStartDir.dx * tmpStraightLen;
|
|
120
108
|
let tmpDepartY = pStart.y + tmpStartDir.dy * tmpStraightLen;
|
|
121
109
|
|
|
122
|
-
// Approach point: offset from target port along its OUTWARD direction
|
|
123
|
-
// (i.e. away from the port, so the line segment goes inward toward the port)
|
|
124
110
|
let tmpApproachX = pEnd.x + tmpEndDir.dx * tmpStraightLen;
|
|
125
111
|
let tmpApproachY = pEnd.y + tmpEndDir.dy * tmpStraightLen;
|
|
126
112
|
|
|
127
|
-
// Now compute the bezier between the departure and approach points
|
|
128
113
|
let tmpDX = Math.abs(tmpApproachX - tmpDepartX);
|
|
129
114
|
let tmpDY = Math.abs(tmpApproachY - tmpDepartY);
|
|
130
115
|
let tmpDist = Math.sqrt(tmpDX * tmpDX + tmpDY * tmpDY);
|
|
131
116
|
|
|
132
|
-
// Base offset for control points
|
|
133
117
|
let tmpBaseOffset = Math.max(Math.min(tmpDist * 0.4, 180), 30);
|
|
134
118
|
|
|
135
119
|
let tmpSameAxis = (tmpStartDir.dx !== 0 && tmpEndDir.dx !== 0) ||
|
|
136
120
|
(tmpStartDir.dy !== 0 && tmpEndDir.dy !== 0);
|
|
137
121
|
|
|
138
|
-
// Check if the ports are "facing each other" on the same axis
|
|
139
122
|
let tmpFacingEachOther = false;
|
|
140
123
|
if (tmpSameAxis)
|
|
141
124
|
{
|
|
@@ -173,18 +156,293 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
173
156
|
tmpCurveOffset = Math.max(tmpBaseOffset * 0.8, 40);
|
|
174
157
|
}
|
|
175
158
|
|
|
176
|
-
// Control points extend outward from the departure/approach points
|
|
177
159
|
let tmpCP1X = tmpDepartX + tmpStartDir.dx * tmpCurveOffset;
|
|
178
160
|
let tmpCP1Y = tmpDepartY + tmpStartDir.dy * tmpCurveOffset;
|
|
179
161
|
let tmpCP2X = tmpApproachX + tmpEndDir.dx * tmpCurveOffset;
|
|
180
162
|
let tmpCP2Y = tmpApproachY + tmpEndDir.dy * tmpCurveOffset;
|
|
181
163
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
164
|
+
return {
|
|
165
|
+
departX: tmpDepartX, departY: tmpDepartY,
|
|
166
|
+
approachX: tmpApproachX, approachY: tmpApproachY,
|
|
167
|
+
cp1X: tmpCP1X, cp1Y: tmpCP1Y,
|
|
168
|
+
cp2X: tmpCP2X, cp2Y: tmpCP2Y,
|
|
169
|
+
startDir: tmpStartDir, endDir: tmpEndDir
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Generate a direction-aware path between two ports.
|
|
175
|
+
*
|
|
176
|
+
* The path is composed of three segments:
|
|
177
|
+
* 1. A short straight "departure" segment leaving the source port
|
|
178
|
+
* in its outward direction.
|
|
179
|
+
* 2. A cubic bezier curve connecting the departure point to an
|
|
180
|
+
* "approach" point near the target port.
|
|
181
|
+
* 3. A short straight "approach" segment arriving at the target
|
|
182
|
+
* port aligned with its inward direction.
|
|
183
|
+
*
|
|
184
|
+
* @param {{x: number, y: number, side: string}} pStart - Start port position + side
|
|
185
|
+
* @param {{x: number, y: number, side: string}} pEnd - End port position + side
|
|
186
|
+
* @returns {string} SVG path d attribute
|
|
187
|
+
*/
|
|
188
|
+
_generateDirectionalPath(pStart, pEnd)
|
|
189
|
+
{
|
|
190
|
+
let tmpGeo = this._computeDirectionalGeometry(pStart, pEnd);
|
|
191
|
+
|
|
192
|
+
return this._FlowView._PathGenerator.buildBezierPathString(
|
|
193
|
+
{ x: pStart.x, y: pStart.y },
|
|
194
|
+
{ x: tmpGeo.departX, y: tmpGeo.departY },
|
|
195
|
+
{ x: tmpGeo.cp1X, y: tmpGeo.cp1Y },
|
|
196
|
+
{ x: tmpGeo.cp2X, y: tmpGeo.cp2Y },
|
|
197
|
+
{ x: tmpGeo.approachX, y: tmpGeo.approachY },
|
|
198
|
+
{ x: pEnd.x, y: pEnd.y }
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Generate a bezier path with an optional user-controlled midpoint handle.
|
|
204
|
+
*
|
|
205
|
+
* If handle is null, uses the standard auto-calculated bezier.
|
|
206
|
+
* If handle is set, splits into two cubic bezier segments passing
|
|
207
|
+
* through the handle point for a smooth S-curve.
|
|
208
|
+
*
|
|
209
|
+
* @param {{x: number, y: number, side: string}} pStart
|
|
210
|
+
* @param {{x: number, y: number, side: string}} pEnd
|
|
211
|
+
* @param {number|null} pHandleX
|
|
212
|
+
* @param {number|null} pHandleY
|
|
213
|
+
* @returns {string} SVG path d attribute
|
|
214
|
+
*/
|
|
215
|
+
_generateBezierPathWithHandle(pStart, pEnd, pHandleX, pHandleY)
|
|
216
|
+
{
|
|
217
|
+
if (pHandleX == null || pHandleY == null)
|
|
218
|
+
{
|
|
219
|
+
return this._generateDirectionalPath(pStart, pEnd);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
let tmpGeo = this._computeDirectionalGeometry(pStart, pEnd);
|
|
223
|
+
|
|
224
|
+
// Split into two cubic bezier segments through the handle point.
|
|
225
|
+
// First segment: depart -> handle
|
|
226
|
+
// Second segment: handle -> approach
|
|
227
|
+
// Control points are computed to ensure smooth tangent at the handle.
|
|
228
|
+
let tmpCP1aX = tmpGeo.departX + tmpGeo.startDir.dx * ((Math.abs(pHandleX - tmpGeo.departX) + Math.abs(pHandleY - tmpGeo.departY)) * 0.4);
|
|
229
|
+
let tmpCP1aY = tmpGeo.departY + tmpGeo.startDir.dy * ((Math.abs(pHandleX - tmpGeo.departX) + Math.abs(pHandleY - tmpGeo.departY)) * 0.4);
|
|
230
|
+
|
|
231
|
+
// The tangent at the handle should be smooth: the control points on
|
|
232
|
+
// either side of the handle should be collinear through it.
|
|
233
|
+
// Use the direction from depart to approach as the tangent direction.
|
|
234
|
+
let tmpTangentX = tmpGeo.approachX - tmpGeo.departX;
|
|
235
|
+
let tmpTangentY = tmpGeo.approachY - tmpGeo.departY;
|
|
236
|
+
let tmpTangentLen = Math.sqrt(tmpTangentX * tmpTangentX + tmpTangentY * tmpTangentY);
|
|
237
|
+
if (tmpTangentLen < 1) tmpTangentLen = 1;
|
|
238
|
+
let tmpTangentNX = tmpTangentX / tmpTangentLen;
|
|
239
|
+
let tmpTangentNY = tmpTangentY / tmpTangentLen;
|
|
240
|
+
|
|
241
|
+
let tmpTangentScale = tmpTangentLen * 0.2;
|
|
242
|
+
|
|
243
|
+
let tmpCP1bX = pHandleX - tmpTangentNX * tmpTangentScale;
|
|
244
|
+
let tmpCP1bY = pHandleY - tmpTangentNY * tmpTangentScale;
|
|
245
|
+
let tmpCP2aX = pHandleX + tmpTangentNX * tmpTangentScale;
|
|
246
|
+
let tmpCP2aY = pHandleY + tmpTangentNY * tmpTangentScale;
|
|
247
|
+
|
|
248
|
+
let tmpCP2bX = tmpGeo.approachX + tmpGeo.endDir.dx * ((Math.abs(pHandleX - tmpGeo.approachX) + Math.abs(pHandleY - tmpGeo.approachY)) * 0.4);
|
|
249
|
+
let tmpCP2bY = tmpGeo.approachY + tmpGeo.endDir.dy * ((Math.abs(pHandleX - tmpGeo.approachX) + Math.abs(pHandleY - tmpGeo.approachY)) * 0.4);
|
|
250
|
+
|
|
251
|
+
return this._FlowView._PathGenerator.buildSplitBezierPathString(
|
|
252
|
+
{ x: pStart.x, y: pStart.y },
|
|
253
|
+
{ x: tmpGeo.departX, y: tmpGeo.departY },
|
|
254
|
+
{ x: tmpCP1aX, y: tmpCP1aY },
|
|
255
|
+
{ x: tmpCP1bX, y: tmpCP1bY },
|
|
256
|
+
{ x: pHandleX, y: pHandleY },
|
|
257
|
+
{ x: tmpCP2aX, y: tmpCP2aY },
|
|
258
|
+
{ x: tmpCP2bX, y: tmpCP2bY },
|
|
259
|
+
{ x: tmpGeo.approachX, y: tmpGeo.approachY },
|
|
260
|
+
{ x: pEnd.x, y: pEnd.y }
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Get the auto-calculated midpoint of the default bezier curve between two ports.
|
|
266
|
+
* Evaluates the cubic bezier at t=0.5.
|
|
267
|
+
*
|
|
268
|
+
* @param {{x: number, y: number, side: string}} pStart
|
|
269
|
+
* @param {{x: number, y: number, side: string}} pEnd
|
|
270
|
+
* @returns {{x: number, y: number}}
|
|
271
|
+
*/
|
|
272
|
+
getAutoMidpoint(pStart, pEnd)
|
|
273
|
+
{
|
|
274
|
+
let tmpGeo = this._computeDirectionalGeometry(pStart, pEnd);
|
|
275
|
+
|
|
276
|
+
return this._FlowView._PathGenerator.evaluateCubicBezier(
|
|
277
|
+
{ x: tmpGeo.departX, y: tmpGeo.departY },
|
|
278
|
+
{ x: tmpGeo.cp1X, y: tmpGeo.cp1Y },
|
|
279
|
+
{ x: tmpGeo.cp2X, y: tmpGeo.cp2Y },
|
|
280
|
+
{ x: tmpGeo.approachX, y: tmpGeo.approachY },
|
|
281
|
+
0.5
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Generate an orthogonal (90-degree angles only) path between two ports.
|
|
287
|
+
*
|
|
288
|
+
* Path format: M start L depart L corner1 L corner2 L approach L end
|
|
289
|
+
*
|
|
290
|
+
* @param {{x: number, y: number, side: string}} pStart
|
|
291
|
+
* @param {{x: number, y: number, side: string}} pEnd
|
|
292
|
+
* @param {Object|null} pCorners - { corner1: {x,y}, corner2: {x,y} } or null for auto
|
|
293
|
+
* @param {number} pMidOffset - Offset for the auto-calculated corridor position
|
|
294
|
+
* @returns {string} SVG path d attribute
|
|
295
|
+
*/
|
|
296
|
+
_generateOrthogonalPath(pStart, pEnd, pCorners, pMidOffset)
|
|
297
|
+
{
|
|
298
|
+
let tmpStartDir = this._FlowView._GeometryProvider.sideDirection(pStart.side || 'right');
|
|
299
|
+
let tmpEndDir = this._FlowView._GeometryProvider.sideDirection(pEnd.side || 'left');
|
|
300
|
+
|
|
301
|
+
let tmpStraightLen = 20;
|
|
302
|
+
|
|
303
|
+
let tmpDepartX = pStart.x + tmpStartDir.dx * tmpStraightLen;
|
|
304
|
+
let tmpDepartY = pStart.y + tmpStartDir.dy * tmpStraightLen;
|
|
305
|
+
|
|
306
|
+
let tmpApproachX = pEnd.x + tmpEndDir.dx * tmpStraightLen;
|
|
307
|
+
let tmpApproachY = pEnd.y + tmpEndDir.dy * tmpStraightLen;
|
|
308
|
+
|
|
309
|
+
let tmpCorner1, tmpCorner2;
|
|
310
|
+
|
|
311
|
+
if (pCorners && pCorners.corner1 && pCorners.corner2)
|
|
312
|
+
{
|
|
313
|
+
tmpCorner1 = pCorners.corner1;
|
|
314
|
+
tmpCorner2 = pCorners.corner2;
|
|
315
|
+
}
|
|
316
|
+
else
|
|
317
|
+
{
|
|
318
|
+
let tmpAutoCorners = this._FlowView._PathGenerator.computeAutoOrthogonalCorners(
|
|
319
|
+
tmpDepartX, tmpDepartY,
|
|
320
|
+
tmpApproachX, tmpApproachY,
|
|
321
|
+
tmpStartDir, tmpEndDir,
|
|
322
|
+
pMidOffset || 0
|
|
323
|
+
);
|
|
324
|
+
tmpCorner1 = tmpAutoCorners.corner1;
|
|
325
|
+
tmpCorner2 = tmpAutoCorners.corner2;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
return this._FlowView._PathGenerator.buildOrthogonalPathString(
|
|
329
|
+
{ x: pStart.x, y: pStart.y },
|
|
330
|
+
{ x: tmpDepartX, y: tmpDepartY },
|
|
331
|
+
tmpCorner1,
|
|
332
|
+
tmpCorner2,
|
|
333
|
+
{ x: tmpApproachX, y: tmpApproachY },
|
|
334
|
+
{ x: pEnd.x, y: pEnd.y }
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Get the full orthogonal geometry for a connection (for handle positioning).
|
|
340
|
+
*
|
|
341
|
+
* @param {{x: number, y: number, side: string}} pStart
|
|
342
|
+
* @param {{x: number, y: number, side: string}} pEnd
|
|
343
|
+
* @param {Object} pData - Connection.Data
|
|
344
|
+
* @returns {{corner1: {x,y}, corner2: {x,y}, midpoint: {x,y}}}
|
|
345
|
+
*/
|
|
346
|
+
getOrthogonalGeometry(pStart, pEnd, pData)
|
|
347
|
+
{
|
|
348
|
+
let tmpStartDir = this._FlowView._GeometryProvider.sideDirection(pStart.side || 'right');
|
|
349
|
+
let tmpEndDir = this._FlowView._GeometryProvider.sideDirection(pEnd.side || 'left');
|
|
350
|
+
|
|
351
|
+
let tmpStraightLen = 20;
|
|
352
|
+
|
|
353
|
+
let tmpDepartX = pStart.x + tmpStartDir.dx * tmpStraightLen;
|
|
354
|
+
let tmpDepartY = pStart.y + tmpStartDir.dy * tmpStraightLen;
|
|
355
|
+
let tmpApproachX = pEnd.x + tmpEndDir.dx * tmpStraightLen;
|
|
356
|
+
let tmpApproachY = pEnd.y + tmpEndDir.dy * tmpStraightLen;
|
|
357
|
+
|
|
358
|
+
if (pData && pData.HandleCustomized && pData.OrthoCorner1X != null)
|
|
359
|
+
{
|
|
360
|
+
let tmpCorner1 = { x: pData.OrthoCorner1X, y: pData.OrthoCorner1Y };
|
|
361
|
+
let tmpCorner2 = { x: pData.OrthoCorner2X, y: pData.OrthoCorner2Y };
|
|
362
|
+
let tmpMidpoint =
|
|
363
|
+
{
|
|
364
|
+
x: (tmpCorner1.x + tmpCorner2.x) / 2,
|
|
365
|
+
y: (tmpCorner1.y + tmpCorner2.y) / 2
|
|
366
|
+
};
|
|
367
|
+
return { corner1: tmpCorner1, corner2: tmpCorner2, midpoint: tmpMidpoint };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
return this._FlowView._PathGenerator.computeAutoOrthogonalCorners(
|
|
371
|
+
tmpDepartX, tmpDepartY,
|
|
372
|
+
tmpApproachX, tmpApproachY,
|
|
373
|
+
tmpStartDir, tmpEndDir,
|
|
374
|
+
(pData && pData.OrthoMidOffset) || 0
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/**
|
|
379
|
+
* Render drag handles for the selected connection.
|
|
380
|
+
*
|
|
381
|
+
* @param {Object} pConnection
|
|
382
|
+
* @param {SVGGElement} pLayer
|
|
383
|
+
* @param {{x, y, side}} pStart - Source port position
|
|
384
|
+
* @param {{x, y, side}} pEnd - Target port position
|
|
385
|
+
*/
|
|
386
|
+
_renderHandles(pConnection, pLayer, pStart, pEnd)
|
|
387
|
+
{
|
|
388
|
+
let tmpData = pConnection.Data || {};
|
|
389
|
+
let tmpLineMode = tmpData.LineMode || 'bezier';
|
|
390
|
+
|
|
391
|
+
if (tmpLineMode === 'orthogonal')
|
|
392
|
+
{
|
|
393
|
+
let tmpGeometry = this.getOrthogonalGeometry(pStart, pEnd, tmpData);
|
|
394
|
+
|
|
395
|
+
// Corner 1 handle
|
|
396
|
+
this._createHandle(pLayer, pConnection.Hash, 'ortho-corner1',
|
|
397
|
+
tmpGeometry.corner1.x, tmpGeometry.corner1.y, 'pict-flow-connection-handle');
|
|
398
|
+
|
|
399
|
+
// Midpoint handle (between corners)
|
|
400
|
+
this._createHandle(pLayer, pConnection.Hash, 'ortho-midpoint',
|
|
401
|
+
tmpGeometry.midpoint.x, tmpGeometry.midpoint.y, 'pict-flow-connection-handle-midpoint');
|
|
402
|
+
|
|
403
|
+
// Corner 2 handle
|
|
404
|
+
this._createHandle(pLayer, pConnection.Hash, 'ortho-corner2',
|
|
405
|
+
tmpGeometry.corner2.x, tmpGeometry.corner2.y, 'pict-flow-connection-handle');
|
|
406
|
+
}
|
|
407
|
+
else
|
|
408
|
+
{
|
|
409
|
+
// Bezier midpoint handle
|
|
410
|
+
let tmpMidpoint;
|
|
411
|
+
if (tmpData.HandleCustomized && tmpData.BezierHandleX != null)
|
|
412
|
+
{
|
|
413
|
+
tmpMidpoint = { x: tmpData.BezierHandleX, y: tmpData.BezierHandleY };
|
|
414
|
+
}
|
|
415
|
+
else
|
|
416
|
+
{
|
|
417
|
+
tmpMidpoint = this.getAutoMidpoint(pStart, pEnd);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
this._createHandle(pLayer, pConnection.Hash, 'bezier-midpoint',
|
|
421
|
+
tmpMidpoint.x, tmpMidpoint.y, 'pict-flow-connection-handle');
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Create a single SVG circle handle element.
|
|
427
|
+
*
|
|
428
|
+
* @param {SVGGElement} pLayer
|
|
429
|
+
* @param {string} pConnectionHash
|
|
430
|
+
* @param {string} pHandleType
|
|
431
|
+
* @param {number} pX
|
|
432
|
+
* @param {number} pY
|
|
433
|
+
* @param {string} pClassName
|
|
434
|
+
*/
|
|
435
|
+
_createHandle(pLayer, pConnectionHash, pHandleType, pX, pY, pClassName)
|
|
436
|
+
{
|
|
437
|
+
let tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
|
|
438
|
+
tmpCircle.setAttribute('class', pClassName);
|
|
439
|
+
tmpCircle.setAttribute('cx', String(pX));
|
|
440
|
+
tmpCircle.setAttribute('cy', String(pY));
|
|
441
|
+
tmpCircle.setAttribute('r', '6');
|
|
442
|
+
tmpCircle.setAttribute('data-element-type', 'connection-handle');
|
|
443
|
+
tmpCircle.setAttribute('data-connection-hash', pConnectionHash);
|
|
444
|
+
tmpCircle.setAttribute('data-handle-type', pHandleType);
|
|
445
|
+
pLayer.appendChild(tmpCircle);
|
|
188
446
|
}
|
|
189
447
|
|
|
190
448
|
/**
|
|
@@ -195,7 +453,7 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
195
453
|
*/
|
|
196
454
|
_generateBezierPath(pStart, pEnd)
|
|
197
455
|
{
|
|
198
|
-
// During drag operations we may not have side info; default to right
|
|
456
|
+
// During drag operations we may not have side info; default to right->left
|
|
199
457
|
let tmpStart = { x: pStart.x, y: pStart.y, side: pStart.side || 'right' };
|
|
200
458
|
let tmpEnd = { x: pEnd.x, y: pEnd.y, side: pEnd.side || 'left' };
|
|
201
459
|
return this._generateDirectionalPath(tmpStart, tmpEnd);
|
|
@@ -218,7 +476,7 @@ class PictServiceFlowConnectionRenderer extends libFableServiceProviderBase
|
|
|
218
476
|
{ x: pEndX, y: pEndY, side: 'left' }
|
|
219
477
|
);
|
|
220
478
|
|
|
221
|
-
let tmpPathElement = this.
|
|
479
|
+
let tmpPathElement = this._FlowView._SVGHelperProvider.createSVGElement('path');
|
|
222
480
|
tmpPathElement.setAttribute('class', 'pict-flow-drag-connection');
|
|
223
481
|
tmpPathElement.setAttribute('d', tmpPath);
|
|
224
482
|
pLayer.appendChild(tmpPathElement);
|