pict-section-flow 0.0.10 → 0.0.13
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 +1 -1
- package/README.md +176 -0
- package/docs/.nojekyll +0 -0
- package/docs/Architecture.md +303 -0
- package/docs/Custom-Styling.md +275 -0
- package/docs/Data_Model.md +158 -0
- package/docs/Event_System.md +156 -0
- package/docs/Getting_Started.md +237 -0
- package/docs/Implementation_Reference.md +528 -0
- package/docs/Layout_Persistence.md +117 -0
- package/docs/README.md +115 -52
- package/docs/_cover.md +11 -0
- package/docs/_sidebar.md +52 -0
- package/docs/_topbar.md +8 -0
- package/docs/api/PictFlowCard.md +216 -0
- package/docs/api/PictFlowCardPropertiesPanel.md +235 -0
- package/docs/api/addConnection.md +101 -0
- package/docs/api/addNode.md +137 -0
- package/docs/api/autoLayout.md +77 -0
- package/docs/api/getFlowData.md +112 -0
- package/docs/api/marshalToView.md +95 -0
- package/docs/api/openPanel.md +128 -0
- package/docs/api/registerHandler.md +174 -0
- package/docs/api/registerNodeType.md +142 -0
- package/docs/api/removeConnection.md +57 -0
- package/docs/api/removeNode.md +80 -0
- package/docs/api/saveLayout.md +152 -0
- package/docs/api/screenToSVGCoords.md +68 -0
- package/docs/api/selectNode.md +116 -0
- package/docs/api/setTheme.md +168 -0
- package/docs/api/setZoom.md +97 -0
- package/docs/api/toggleFullscreen.md +68 -0
- package/docs/card-help/EACH.md +19 -0
- package/docs/card-help/FREAD.md +24 -0
- package/docs/card-help/FWRITE.md +24 -0
- package/docs/card-help/GET.md +22 -0
- package/docs/card-help/ITE.md +23 -0
- package/docs/card-help/LOG.md +23 -0
- package/docs/card-help/NOTE.md +17 -0
- package/docs/card-help/PREV.md +18 -0
- package/docs/card-help/SET.md +27 -0
- package/docs/card-help/SPKL.md +22 -0
- package/docs/card-help/STAT.md +23 -0
- package/docs/card-help/SW.md +25 -0
- package/docs/css/docuserve.css +73 -0
- package/docs/index.html +39 -0
- package/docs/retold-catalog.json +169 -0
- package/docs/retold-keyword-index.json +13942 -0
- package/example_applications/simple_cards/package.json +1 -0
- package/example_applications/simple_cards/source/card-help-content.js +16 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Comment.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-DataPreview.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Each.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-FileRead.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-FileWrite.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-GetValue.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-IfThenElse.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-LogValues.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-SetValue.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Sparkline.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-StatusMonitor.js +2 -0
- package/example_applications/simple_cards/source/cards/FlowCard-Switch.js +2 -0
- package/package.json +11 -7
- package/scripts/generate-card-help.js +214 -0
- package/source/Pict-Section-Flow.js +4 -0
- package/source/PictFlowCard.js +3 -1
- package/source/providers/PictProvider-Flow-CSS.js +245 -152
- package/source/providers/PictProvider-Flow-ConnectorShapes.js +24 -0
- package/source/providers/PictProvider-Flow-Geometry.js +195 -38
- package/source/providers/PictProvider-Flow-PanelChrome.js +14 -12
- package/source/services/PictService-Flow-ConnectionHandleManager.js +263 -0
- package/source/services/PictService-Flow-ConnectionRenderer.js +134 -183
- package/source/services/PictService-Flow-DataManager.js +338 -0
- package/source/services/PictService-Flow-InteractionManager.js +165 -7
- package/source/services/PictService-Flow-PathGenerator.js +282 -0
- package/source/services/PictService-Flow-PortRenderer.js +269 -0
- package/source/services/PictService-Flow-RenderManager.js +281 -0
- package/source/services/PictService-Flow-Tether.js +6 -42
- package/source/views/PictView-Flow-Node.js +2 -220
- package/source/views/PictView-Flow-PropertiesPanel.js +89 -44
- package/source/views/PictView-Flow.js +130 -882
- package/test/ConnectionHandleManager_tests.js +717 -0
- package/test/ConnectionRenderer_tests.js +591 -0
- package/test/DataManager_tests.js +859 -0
- package/test/Geometry_tests.js +767 -0
- package/test/PathGenerator_tests.js +978 -0
- package/test/PortRenderer_tests.js +367 -0
- package/test/RenderManager_tests.js +756 -0
|
@@ -178,6 +178,105 @@ class PictServiceFlowPathGenerator extends libFableServiceProviderBase
|
|
|
178
178
|
return `M ${pStart.x} ${pStart.y} L ${pDepart.x} ${pDepart.y} C ${pCP1a.x} ${pCP1a.y}, ${pCP1b.x} ${pCP1b.y}, ${pHandle.x} ${pHandle.y} C ${pCP2a.x} ${pCP2a.y}, ${pCP2b.x} ${pCP2b.y}, ${pApproach.x} ${pApproach.y} L ${pEnd.x} ${pEnd.y}`;
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
+
/**
|
|
182
|
+
* Build an SVG multi-segment bezier path string.
|
|
183
|
+
* Generates N+1 cubic bezier segments through N handle points.
|
|
184
|
+
*
|
|
185
|
+
* Pattern: M start L depart C cp,cp,handle[0] C cp,cp,handle[1] ... C cp,cp,approach L end
|
|
186
|
+
*
|
|
187
|
+
* Control points are computed using Catmull-Rom-to-Bezier conversion
|
|
188
|
+
* for C1 (smooth tangent) continuity at every handle.
|
|
189
|
+
*
|
|
190
|
+
* @param {{x: number, y: number}} pStart - Port anchor start
|
|
191
|
+
* @param {{x: number, y: number}} pDepart - Departure point after straight segment
|
|
192
|
+
* @param {Array<{x: number, y: number}>} pHandles - Ordered handle waypoints
|
|
193
|
+
* @param {{x: number, y: number}} pApproach - Approach point before final straight segment
|
|
194
|
+
* @param {{x: number, y: number}} pEnd - Port anchor end
|
|
195
|
+
* @param {{dx: number, dy: number}} pStartDir - Departure direction unit vector
|
|
196
|
+
* @param {{dx: number, dy: number}} pEndDir - Approach direction unit vector
|
|
197
|
+
* @returns {string} SVG path d attribute
|
|
198
|
+
*/
|
|
199
|
+
buildMultiBezierPathString(pStart, pDepart, pHandles, pApproach, pEnd, pStartDir, pEndDir)
|
|
200
|
+
{
|
|
201
|
+
// Build the full list of waypoints: depart, handle[0..N-1], approach
|
|
202
|
+
let tmpWaypoints = [pDepart];
|
|
203
|
+
for (let i = 0; i < pHandles.length; i++)
|
|
204
|
+
{
|
|
205
|
+
tmpWaypoints.push(pHandles[i]);
|
|
206
|
+
}
|
|
207
|
+
tmpWaypoints.push(pApproach);
|
|
208
|
+
|
|
209
|
+
let tmpPath = `M ${pStart.x} ${pStart.y} L ${pDepart.x} ${pDepart.y}`;
|
|
210
|
+
|
|
211
|
+
for (let i = 0; i < tmpWaypoints.length - 1; i++)
|
|
212
|
+
{
|
|
213
|
+
let tmpFrom = tmpWaypoints[i];
|
|
214
|
+
let tmpTo = tmpWaypoints[i + 1];
|
|
215
|
+
|
|
216
|
+
let tmpSegDX = tmpTo.x - tmpFrom.x;
|
|
217
|
+
let tmpSegDY = tmpTo.y - tmpFrom.y;
|
|
218
|
+
let tmpSegLen = Math.sqrt(tmpSegDX * tmpSegDX + tmpSegDY * tmpSegDY);
|
|
219
|
+
if (tmpSegLen < 1)
|
|
220
|
+
{
|
|
221
|
+
tmpSegLen = 1;
|
|
222
|
+
}
|
|
223
|
+
let tmpScale = tmpSegLen * 0.35;
|
|
224
|
+
|
|
225
|
+
// Tangent at tmpFrom
|
|
226
|
+
let tmpTanFromX, tmpTanFromY;
|
|
227
|
+
if (i === 0)
|
|
228
|
+
{
|
|
229
|
+
// First segment: use the port departure direction
|
|
230
|
+
tmpTanFromX = pStartDir.dx;
|
|
231
|
+
tmpTanFromY = pStartDir.dy;
|
|
232
|
+
}
|
|
233
|
+
else
|
|
234
|
+
{
|
|
235
|
+
// Interior handle: tangent points from previous toward next waypoint
|
|
236
|
+
let tmpPrev = tmpWaypoints[i - 1];
|
|
237
|
+
let tmpNext = tmpWaypoints[i + 1];
|
|
238
|
+
tmpTanFromX = tmpNext.x - tmpPrev.x;
|
|
239
|
+
tmpTanFromY = tmpNext.y - tmpPrev.y;
|
|
240
|
+
let tmpTanLen = Math.sqrt(tmpTanFromX * tmpTanFromX + tmpTanFromY * tmpTanFromY);
|
|
241
|
+
if (tmpTanLen < 1) tmpTanLen = 1;
|
|
242
|
+
tmpTanFromX /= tmpTanLen;
|
|
243
|
+
tmpTanFromY /= tmpTanLen;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Tangent at tmpTo
|
|
247
|
+
let tmpTanToX, tmpTanToY;
|
|
248
|
+
if (i === tmpWaypoints.length - 2)
|
|
249
|
+
{
|
|
250
|
+
// Last segment: use the port approach direction (reversed for incoming)
|
|
251
|
+
tmpTanToX = -pEndDir.dx;
|
|
252
|
+
tmpTanToY = -pEndDir.dy;
|
|
253
|
+
}
|
|
254
|
+
else
|
|
255
|
+
{
|
|
256
|
+
// Interior handle: tangent points from previous toward next waypoint
|
|
257
|
+
let tmpPrev = tmpWaypoints[i];
|
|
258
|
+
let tmpNext = tmpWaypoints[i + 2];
|
|
259
|
+
tmpTanToX = tmpNext.x - tmpPrev.x;
|
|
260
|
+
tmpTanToY = tmpNext.y - tmpPrev.y;
|
|
261
|
+
let tmpTanLen = Math.sqrt(tmpTanToX * tmpTanToX + tmpTanToY * tmpTanToY);
|
|
262
|
+
if (tmpTanLen < 1) tmpTanLen = 1;
|
|
263
|
+
tmpTanToX /= tmpTanLen;
|
|
264
|
+
tmpTanToY /= tmpTanLen;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
let tmpCP1X = tmpFrom.x + tmpTanFromX * tmpScale;
|
|
268
|
+
let tmpCP1Y = tmpFrom.y + tmpTanFromY * tmpScale;
|
|
269
|
+
let tmpCP2X = tmpTo.x - tmpTanToX * tmpScale;
|
|
270
|
+
let tmpCP2Y = tmpTo.y - tmpTanToY * tmpScale;
|
|
271
|
+
|
|
272
|
+
tmpPath += ` C ${tmpCP1X} ${tmpCP1Y}, ${tmpCP2X} ${tmpCP2Y}, ${tmpTo.x} ${tmpTo.y}`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
tmpPath += ` L ${pEnd.x} ${pEnd.y}`;
|
|
276
|
+
|
|
277
|
+
return tmpPath;
|
|
278
|
+
}
|
|
279
|
+
|
|
181
280
|
/**
|
|
182
281
|
* Build an SVG orthogonal (right-angle) path string.
|
|
183
282
|
* Pattern: M start L depart L corner1 L corner2 L approach L end
|
|
@@ -194,6 +293,189 @@ class PictServiceFlowPathGenerator extends libFableServiceProviderBase
|
|
|
194
293
|
{
|
|
195
294
|
return `M ${pStart.x} ${pStart.y} L ${pDepart.x} ${pDepart.y} L ${pCorner1.x} ${pCorner1.y} L ${pCorner2.x} ${pCorner2.y} L ${pApproach.x} ${pApproach.y} L ${pEnd.x} ${pEnd.y}`;
|
|
196
295
|
}
|
|
296
|
+
|
|
297
|
+
// ---- Directional Geometry ----
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Compute full directional geometry between two port anchors, including
|
|
301
|
+
* departure/approach points and bezier control points.
|
|
302
|
+
*
|
|
303
|
+
* Uses sophisticated facing detection: when ports face each other the
|
|
304
|
+
* curve offset scales with inline distance; when ports are on the same
|
|
305
|
+
* axis but not facing, a wider offset prevents the path from collapsing;
|
|
306
|
+
* perpendicular exits use a moderate offset.
|
|
307
|
+
*
|
|
308
|
+
* @param {{x: number, y: number, side: string}} pStart
|
|
309
|
+
* @param {{x: number, y: number, side: string}} pEnd
|
|
310
|
+
* @returns {{departX: number, departY: number, approachX: number, approachY: number, cp1X: number, cp1Y: number, cp2X: number, cp2Y: number, startDir: {dx: number, dy: number}, endDir: {dx: number, dy: number}}}
|
|
311
|
+
*/
|
|
312
|
+
computeDirectionalGeometry(pStart, pEnd)
|
|
313
|
+
{
|
|
314
|
+
let tmpStartDir = this._FlowView._GeometryProvider.sideDirection(pStart.side || 'right');
|
|
315
|
+
let tmpEndDir = this._FlowView._GeometryProvider.sideDirection(pEnd.side || 'left');
|
|
316
|
+
|
|
317
|
+
let tmpStraightLen = 20;
|
|
318
|
+
|
|
319
|
+
let tmpDepartX = pStart.x + tmpStartDir.dx * tmpStraightLen;
|
|
320
|
+
let tmpDepartY = pStart.y + tmpStartDir.dy * tmpStraightLen;
|
|
321
|
+
|
|
322
|
+
let tmpApproachX = pEnd.x + tmpEndDir.dx * tmpStraightLen;
|
|
323
|
+
let tmpApproachY = pEnd.y + tmpEndDir.dy * tmpStraightLen;
|
|
324
|
+
|
|
325
|
+
let tmpDX = Math.abs(tmpApproachX - tmpDepartX);
|
|
326
|
+
let tmpDY = Math.abs(tmpApproachY - tmpDepartY);
|
|
327
|
+
let tmpDist = Math.sqrt(tmpDX * tmpDX + tmpDY * tmpDY);
|
|
328
|
+
|
|
329
|
+
let tmpBaseOffset = Math.max(Math.min(tmpDist * 0.4, 180), 30);
|
|
330
|
+
|
|
331
|
+
let tmpSameAxis = (tmpStartDir.dx !== 0 && tmpEndDir.dx !== 0) ||
|
|
332
|
+
(tmpStartDir.dy !== 0 && tmpEndDir.dy !== 0);
|
|
333
|
+
|
|
334
|
+
let tmpFacingEachOther = false;
|
|
335
|
+
if (tmpSameAxis)
|
|
336
|
+
{
|
|
337
|
+
if (tmpStartDir.dx === 1 && tmpEndDir.dx === -1 && pEnd.x >= pStart.x)
|
|
338
|
+
{
|
|
339
|
+
tmpFacingEachOther = true;
|
|
340
|
+
}
|
|
341
|
+
else if (tmpStartDir.dx === -1 && tmpEndDir.dx === 1 && pEnd.x <= pStart.x)
|
|
342
|
+
{
|
|
343
|
+
tmpFacingEachOther = true;
|
|
344
|
+
}
|
|
345
|
+
else if (tmpStartDir.dy === 1 && tmpEndDir.dy === -1 && pEnd.y >= pStart.y)
|
|
346
|
+
{
|
|
347
|
+
tmpFacingEachOther = true;
|
|
348
|
+
}
|
|
349
|
+
else if (tmpStartDir.dy === -1 && tmpEndDir.dy === 1 && pEnd.y <= pStart.y)
|
|
350
|
+
{
|
|
351
|
+
tmpFacingEachOther = true;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
let tmpCurveOffset;
|
|
356
|
+
|
|
357
|
+
if (tmpFacingEachOther)
|
|
358
|
+
{
|
|
359
|
+
let tmpInlineDist = (tmpStartDir.dx !== 0) ? tmpDX : tmpDY;
|
|
360
|
+
tmpCurveOffset = Math.max(tmpInlineDist * 0.35, 30);
|
|
361
|
+
}
|
|
362
|
+
else if (tmpSameAxis)
|
|
363
|
+
{
|
|
364
|
+
tmpCurveOffset = Math.max(tmpBaseOffset, 60);
|
|
365
|
+
}
|
|
366
|
+
else
|
|
367
|
+
{
|
|
368
|
+
tmpCurveOffset = Math.max(tmpBaseOffset * 0.8, 40);
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let tmpCP1X = tmpDepartX + tmpStartDir.dx * tmpCurveOffset;
|
|
372
|
+
let tmpCP1Y = tmpDepartY + tmpStartDir.dy * tmpCurveOffset;
|
|
373
|
+
let tmpCP2X = tmpApproachX + tmpEndDir.dx * tmpCurveOffset;
|
|
374
|
+
let tmpCP2Y = tmpApproachY + tmpEndDir.dy * tmpCurveOffset;
|
|
375
|
+
|
|
376
|
+
return {
|
|
377
|
+
departX: tmpDepartX, departY: tmpDepartY,
|
|
378
|
+
approachX: tmpApproachX, approachY: tmpApproachY,
|
|
379
|
+
cp1X: tmpCP1X, cp1Y: tmpCP1Y,
|
|
380
|
+
cp2X: tmpCP2X, cp2Y: tmpCP2Y,
|
|
381
|
+
startDir: tmpStartDir, endDir: tmpEndDir
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ---- Distance Utilities ----
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Distance from point (pPX, pPY) to line segment (pAX, pAY)-(pBX, pBY).
|
|
389
|
+
* Pure math utility, no state.
|
|
390
|
+
*
|
|
391
|
+
* @param {number} pPX
|
|
392
|
+
* @param {number} pPY
|
|
393
|
+
* @param {number} pAX
|
|
394
|
+
* @param {number} pAY
|
|
395
|
+
* @param {number} pBX
|
|
396
|
+
* @param {number} pBY
|
|
397
|
+
* @returns {number}
|
|
398
|
+
*/
|
|
399
|
+
distanceToSegment(pPX, pPY, pAX, pAY, pBX, pBY)
|
|
400
|
+
{
|
|
401
|
+
let tmpDX = pBX - pAX;
|
|
402
|
+
let tmpDY = pBY - pAY;
|
|
403
|
+
let tmpLenSq = tmpDX * tmpDX + tmpDY * tmpDY;
|
|
404
|
+
|
|
405
|
+
if (tmpLenSq < 0.001)
|
|
406
|
+
{
|
|
407
|
+
// Degenerate segment
|
|
408
|
+
let tmpDPX = pPX - pAX;
|
|
409
|
+
let tmpDPY = pPY - pAY;
|
|
410
|
+
return Math.sqrt(tmpDPX * tmpDPX + tmpDPY * tmpDPY);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// Project point onto segment, clamped to [0, 1]
|
|
414
|
+
let tmpT = ((pPX - pAX) * tmpDX + (pPY - pAY) * tmpDY) / tmpLenSq;
|
|
415
|
+
if (tmpT < 0) tmpT = 0;
|
|
416
|
+
if (tmpT > 1) tmpT = 1;
|
|
417
|
+
|
|
418
|
+
let tmpClosestX = pAX + tmpT * tmpDX;
|
|
419
|
+
let tmpClosestY = pAY + tmpT * tmpDY;
|
|
420
|
+
let tmpDistX = pPX - tmpClosestX;
|
|
421
|
+
let tmpDistY = pPY - tmpClosestY;
|
|
422
|
+
return Math.sqrt(tmpDistX * tmpDistX + tmpDistY * tmpDistY);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ---- Auto Midpoint Calculation ----
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Get the auto-calculated midpoint of the default bezier curve between
|
|
429
|
+
* two port anchors, using the full directional geometry (facing detection,
|
|
430
|
+
* adaptive curve offsets). Evaluates the cubic bezier at t=0.5.
|
|
431
|
+
*
|
|
432
|
+
* Used by ConnectionRenderer for connection midpoints.
|
|
433
|
+
*
|
|
434
|
+
* @param {{x: number, y: number, side: string}} pStart
|
|
435
|
+
* @param {{x: number, y: number, side: string}} pEnd
|
|
436
|
+
* @returns {{x: number, y: number}}
|
|
437
|
+
*/
|
|
438
|
+
getAutoMidpoint(pStart, pEnd)
|
|
439
|
+
{
|
|
440
|
+
let tmpGeo = this.computeDirectionalGeometry(pStart, pEnd);
|
|
441
|
+
|
|
442
|
+
return this.evaluateCubicBezier(
|
|
443
|
+
{ x: tmpGeo.departX, y: tmpGeo.departY },
|
|
444
|
+
{ x: tmpGeo.cp1X, y: tmpGeo.cp1Y },
|
|
445
|
+
{ x: tmpGeo.cp2X, y: tmpGeo.cp2Y },
|
|
446
|
+
{ x: tmpGeo.approachX, y: tmpGeo.approachY },
|
|
447
|
+
0.5
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
/**
|
|
452
|
+
* Get the auto-calculated midpoint using simple span-based control points.
|
|
453
|
+
* Uses computeDepartApproach for basic geometry, then span * 0.4 for
|
|
454
|
+
* control point distance. Evaluates the cubic bezier at t=0.5.
|
|
455
|
+
*
|
|
456
|
+
* Used by TetherService for tether midpoints.
|
|
457
|
+
*
|
|
458
|
+
* @param {{x: number, y: number, side: string}} pFrom
|
|
459
|
+
* @param {{x: number, y: number, side: string}} pTo
|
|
460
|
+
* @param {number} pDepartDist - Departure/approach distance
|
|
461
|
+
* @returns {{x: number, y: number}}
|
|
462
|
+
*/
|
|
463
|
+
getAutoMidpointSimple(pFrom, pTo, pDepartDist)
|
|
464
|
+
{
|
|
465
|
+
let tmpDA = this.computeDepartApproach(pFrom, pTo, pDepartDist);
|
|
466
|
+
|
|
467
|
+
let tmpSpanX = Math.abs(tmpDA.approachX - tmpDA.departX);
|
|
468
|
+
let tmpSpanY = Math.abs(tmpDA.approachY - tmpDA.departY);
|
|
469
|
+
let tmpSpan = Math.max(tmpSpanX, tmpSpanY, 40);
|
|
470
|
+
let tmpCPDist = tmpSpan * 0.4;
|
|
471
|
+
|
|
472
|
+
let tmpP0 = { x: tmpDA.departX, y: tmpDA.departY };
|
|
473
|
+
let tmpP1 = { x: tmpDA.departX + tmpDA.fromDir.dx * tmpCPDist, y: tmpDA.departY + tmpDA.fromDir.dy * tmpCPDist };
|
|
474
|
+
let tmpP2 = { x: tmpDA.approachX + tmpDA.toDir.dx * tmpCPDist, y: tmpDA.approachY + tmpDA.toDir.dy * tmpCPDist };
|
|
475
|
+
let tmpP3 = { x: tmpDA.approachX, y: tmpDA.approachY };
|
|
476
|
+
|
|
477
|
+
return this.evaluateCubicBezier(tmpP0, tmpP1, tmpP2, tmpP3, 0.5);
|
|
478
|
+
}
|
|
197
479
|
}
|
|
198
480
|
|
|
199
481
|
module.exports = PictServiceFlowPathGenerator;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
const libFableServiceProviderBase = require('fable-serviceproviderbase');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* PictService-Flow-PortRenderer
|
|
5
|
+
*
|
|
6
|
+
* Renders port circles, labels, and badges for flow diagram nodes.
|
|
7
|
+
*
|
|
8
|
+
* Extracted from PictView-Flow-Node.js to isolate port rendering logic
|
|
9
|
+
* from node body rendering and layout.
|
|
10
|
+
*
|
|
11
|
+
* Dependencies (all accessed via this._FlowView):
|
|
12
|
+
* - _GeometryProvider — for getEdgeFromSide, getPortLocalPosition
|
|
13
|
+
* - _ConnectorShapesProvider — for createPortElement
|
|
14
|
+
* - _SVGHelperProvider — for createSVGElement
|
|
15
|
+
*/
|
|
16
|
+
class PictServiceFlowPortRenderer extends libFableServiceProviderBase
|
|
17
|
+
{
|
|
18
|
+
constructor(pFable, pOptions, pServiceHash)
|
|
19
|
+
{
|
|
20
|
+
super(pFable, pOptions, pServiceHash);
|
|
21
|
+
|
|
22
|
+
this.serviceType = 'PictServiceFlowPortRenderer';
|
|
23
|
+
|
|
24
|
+
this._FlowView = (pOptions && pOptions.FlowView) ? pOptions.FlowView : null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Render ports for a node.
|
|
29
|
+
* @param {Object} pNodeData
|
|
30
|
+
* @param {SVGGElement} pGroup - The node's SVG group
|
|
31
|
+
* @param {number} pWidth
|
|
32
|
+
* @param {number} pHeight
|
|
33
|
+
* @param {Object} [pNodeTypeConfig] - Node type configuration (for label display options)
|
|
34
|
+
* @param {number} pNodeTitleBarHeight - Title bar height (for port position offset)
|
|
35
|
+
*/
|
|
36
|
+
renderPorts(pNodeData, pGroup, pWidth, pHeight, pNodeTypeConfig, pNodeTitleBarHeight)
|
|
37
|
+
{
|
|
38
|
+
if (!this._FlowView) return;
|
|
39
|
+
if (!pNodeData.Ports || !Array.isArray(pNodeData.Ports)) return;
|
|
40
|
+
|
|
41
|
+
let tmpPortLabelsVertical = (pNodeTypeConfig && pNodeTypeConfig.PortLabelsVertical);
|
|
42
|
+
let tmpPortLabelPadding = (pNodeTypeConfig && pNodeTypeConfig.PortLabelPadding);
|
|
43
|
+
let tmpPortLabelsOutside = (pNodeTypeConfig && pNodeTypeConfig.PortLabelsOutside);
|
|
44
|
+
let tmpGeometryProvider = this._FlowView._GeometryProvider;
|
|
45
|
+
|
|
46
|
+
// Group ports by their Side value (supports all 12 positions)
|
|
47
|
+
let tmpPortsBySide = {};
|
|
48
|
+
for (let i = 0; i < pNodeData.Ports.length; i++)
|
|
49
|
+
{
|
|
50
|
+
let tmpPort = pNodeData.Ports[i];
|
|
51
|
+
let tmpSide = tmpPort.Side || (tmpPort.Direction === 'input' ? 'left' : 'right');
|
|
52
|
+
if (!tmpPortsBySide[tmpSide])
|
|
53
|
+
{
|
|
54
|
+
tmpPortsBySide[tmpSide] = [];
|
|
55
|
+
}
|
|
56
|
+
tmpPortsBySide[tmpSide].push(tmpPort);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Build port counts map for adaptive zone sizing
|
|
60
|
+
let tmpPortCountsBySide = {};
|
|
61
|
+
for (let tmpKey in tmpPortsBySide)
|
|
62
|
+
{
|
|
63
|
+
tmpPortCountsBySide[tmpKey] = tmpPortsBySide[tmpKey].length;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (let tmpSide in tmpPortsBySide)
|
|
67
|
+
{
|
|
68
|
+
let tmpPorts = tmpPortsBySide[tmpSide];
|
|
69
|
+
// Determine the edge for label positioning
|
|
70
|
+
let tmpEdge = tmpGeometryProvider ? tmpGeometryProvider.getEdgeFromSide(tmpSide) : tmpSide;
|
|
71
|
+
|
|
72
|
+
for (let i = 0; i < tmpPorts.length; i++)
|
|
73
|
+
{
|
|
74
|
+
let tmpPort = tmpPorts[i];
|
|
75
|
+
let tmpPosition = this.getPortLocalPosition(tmpSide, i, tmpPorts.length, pWidth, pHeight, pNodeTitleBarHeight, tmpPortCountsBySide);
|
|
76
|
+
|
|
77
|
+
// Port label badge — flush against the node edge with no
|
|
78
|
+
// border on the edge side; rendered before the port circle
|
|
79
|
+
// so the circle visually sits on top of the badge
|
|
80
|
+
let tmpLabelElement = null;
|
|
81
|
+
if (tmpPort.Label)
|
|
82
|
+
{
|
|
83
|
+
let tmpPortTypeColorMap =
|
|
84
|
+
{
|
|
85
|
+
'event-in': '#3498db',
|
|
86
|
+
'event-out': '#2ecc71',
|
|
87
|
+
'setting': '#e67e22',
|
|
88
|
+
'value': '#f1c40f',
|
|
89
|
+
'error': '#e74c3c'
|
|
90
|
+
};
|
|
91
|
+
let tmpBorderColor = tmpPort.PortType ? (tmpPortTypeColorMap[tmpPort.PortType] || '#95a5a6') : '#95a5a6';
|
|
92
|
+
|
|
93
|
+
let tmpBadgeHeight = 12;
|
|
94
|
+
let tmpBadgePadH = 5;
|
|
95
|
+
let tmpBadgeBorderW = 2;
|
|
96
|
+
let tmpEdgePad = 1;
|
|
97
|
+
let tmpPortRadius = 5;
|
|
98
|
+
|
|
99
|
+
let tmpTextLen = tmpPort.Label.length * 5;
|
|
100
|
+
let tmpBadgeX, tmpBadgeY, tmpBadgeWidth;
|
|
101
|
+
let tmpTextX, tmpTextAnchor;
|
|
102
|
+
let tmpStripeX, tmpStripeY, tmpStripeW, tmpStripeH;
|
|
103
|
+
let tmpBorderPath;
|
|
104
|
+
|
|
105
|
+
if (tmpEdge === 'left')
|
|
106
|
+
{
|
|
107
|
+
tmpBadgeWidth = tmpPortRadius + tmpBadgePadH + tmpTextLen + tmpBadgePadH + tmpBadgeBorderW;
|
|
108
|
+
tmpBadgeX = tmpEdgePad;
|
|
109
|
+
tmpBadgeY = tmpPosition.y - tmpBadgeHeight / 2;
|
|
110
|
+
tmpTextX = tmpBadgeX + tmpPortRadius + tmpBadgePadH;
|
|
111
|
+
tmpTextAnchor = 'start';
|
|
112
|
+
tmpStripeX = tmpBadgeX + tmpBadgeWidth - tmpBadgeBorderW;
|
|
113
|
+
tmpStripeY = tmpBadgeY;
|
|
114
|
+
tmpStripeW = tmpBadgeBorderW;
|
|
115
|
+
tmpStripeH = tmpBadgeHeight;
|
|
116
|
+
tmpBorderPath = 'M ' + tmpBadgeX + ' ' + tmpBadgeY
|
|
117
|
+
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
|
|
118
|
+
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
119
|
+
+ ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight);
|
|
120
|
+
}
|
|
121
|
+
else if (tmpEdge === 'right')
|
|
122
|
+
{
|
|
123
|
+
tmpBadgeWidth = tmpBadgeBorderW + tmpBadgePadH + tmpTextLen + tmpBadgePadH + tmpPortRadius;
|
|
124
|
+
tmpBadgeX = pWidth - tmpBadgeWidth - tmpEdgePad;
|
|
125
|
+
tmpBadgeY = tmpPosition.y - tmpBadgeHeight / 2;
|
|
126
|
+
tmpTextX = tmpBadgeX + tmpBadgeBorderW + tmpBadgePadH;
|
|
127
|
+
tmpTextAnchor = 'start';
|
|
128
|
+
tmpStripeX = tmpBadgeX;
|
|
129
|
+
tmpStripeY = tmpBadgeY;
|
|
130
|
+
tmpStripeW = tmpBadgeBorderW;
|
|
131
|
+
tmpStripeH = tmpBadgeHeight;
|
|
132
|
+
tmpBorderPath = 'M ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
|
|
133
|
+
+ ' L ' + tmpBadgeX + ' ' + tmpBadgeY
|
|
134
|
+
+ ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
135
|
+
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight);
|
|
136
|
+
}
|
|
137
|
+
else if (tmpEdge === 'top')
|
|
138
|
+
{
|
|
139
|
+
tmpBadgeWidth = tmpTextLen + tmpBadgePadH * 2;
|
|
140
|
+
tmpBadgeX = tmpPosition.x - tmpBadgeWidth / 2;
|
|
141
|
+
tmpBadgeY = tmpEdgePad;
|
|
142
|
+
tmpTextX = tmpPosition.x;
|
|
143
|
+
tmpTextAnchor = 'middle';
|
|
144
|
+
tmpStripeX = tmpBadgeX;
|
|
145
|
+
tmpStripeY = tmpBadgeY + tmpBadgeHeight - tmpBadgeBorderW;
|
|
146
|
+
tmpStripeW = tmpBadgeWidth;
|
|
147
|
+
tmpStripeH = tmpBadgeBorderW;
|
|
148
|
+
tmpBorderPath = 'M ' + tmpBadgeX + ' ' + tmpBadgeY
|
|
149
|
+
+ ' L ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
150
|
+
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
151
|
+
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY;
|
|
152
|
+
}
|
|
153
|
+
else
|
|
154
|
+
{
|
|
155
|
+
tmpBadgeWidth = tmpTextLen + tmpBadgePadH * 2;
|
|
156
|
+
tmpBadgeX = tmpPosition.x - tmpBadgeWidth / 2;
|
|
157
|
+
tmpBadgeY = pHeight - tmpBadgeHeight - tmpEdgePad;
|
|
158
|
+
tmpTextX = tmpPosition.x;
|
|
159
|
+
tmpTextAnchor = 'middle';
|
|
160
|
+
tmpStripeX = tmpBadgeX;
|
|
161
|
+
tmpStripeY = tmpBadgeY;
|
|
162
|
+
tmpStripeW = tmpBadgeWidth;
|
|
163
|
+
tmpStripeH = tmpBadgeBorderW;
|
|
164
|
+
tmpBorderPath = 'M ' + tmpBadgeX + ' ' + (tmpBadgeY + tmpBadgeHeight)
|
|
165
|
+
+ ' L ' + tmpBadgeX + ' ' + tmpBadgeY
|
|
166
|
+
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + tmpBadgeY
|
|
167
|
+
+ ' L ' + (tmpBadgeX + tmpBadgeWidth) + ' ' + (tmpBadgeY + tmpBadgeHeight);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Background rect (cream, no stroke — border drawn separately)
|
|
171
|
+
let tmpBgRect = this._FlowView._SVGHelperProvider.createSVGElement('rect');
|
|
172
|
+
tmpBgRect.setAttribute('class', 'pict-flow-port-label-bg');
|
|
173
|
+
tmpBgRect.setAttribute('x', String(tmpBadgeX));
|
|
174
|
+
tmpBgRect.setAttribute('y', String(tmpBadgeY));
|
|
175
|
+
tmpBgRect.setAttribute('width', String(tmpBadgeWidth));
|
|
176
|
+
tmpBgRect.setAttribute('height', String(tmpBadgeHeight));
|
|
177
|
+
tmpBgRect.setAttribute('fill', 'var(--pf-port-label-bg, rgba(255, 253, 240, 0.5))');
|
|
178
|
+
pGroup.appendChild(tmpBgRect);
|
|
179
|
+
|
|
180
|
+
// 3-sided border path (open on the edge-facing side)
|
|
181
|
+
let tmpBorderPathEl = this._FlowView._SVGHelperProvider.createSVGElement('path');
|
|
182
|
+
tmpBorderPathEl.setAttribute('class', 'pict-flow-port-label-bg');
|
|
183
|
+
tmpBorderPathEl.setAttribute('d', tmpBorderPath);
|
|
184
|
+
tmpBorderPathEl.setAttribute('fill', 'none');
|
|
185
|
+
tmpBorderPathEl.setAttribute('stroke', tmpBorderColor);
|
|
186
|
+
tmpBorderPathEl.setAttribute('stroke-width', '0.75');
|
|
187
|
+
pGroup.appendChild(tmpBorderPathEl);
|
|
188
|
+
|
|
189
|
+
// Colored stripe on the inner side
|
|
190
|
+
let tmpStripe = this._FlowView._SVGHelperProvider.createSVGElement('rect');
|
|
191
|
+
tmpStripe.setAttribute('class', 'pict-flow-port-label-bg');
|
|
192
|
+
tmpStripe.setAttribute('x', String(tmpStripeX));
|
|
193
|
+
tmpStripe.setAttribute('y', String(tmpStripeY));
|
|
194
|
+
tmpStripe.setAttribute('width', String(tmpStripeW));
|
|
195
|
+
tmpStripe.setAttribute('height', String(tmpStripeH));
|
|
196
|
+
tmpStripe.setAttribute('fill', tmpBorderColor);
|
|
197
|
+
pGroup.appendChild(tmpStripe);
|
|
198
|
+
|
|
199
|
+
// Text label — appended after circle for z-order
|
|
200
|
+
tmpLabelElement = this._FlowView._SVGHelperProvider.createSVGElement('text');
|
|
201
|
+
tmpLabelElement.setAttribute('class', 'pict-flow-port-label');
|
|
202
|
+
tmpLabelElement.setAttribute('fill', 'var(--pf-port-label-text, #2c3e50)');
|
|
203
|
+
tmpLabelElement.textContent = tmpPort.Label;
|
|
204
|
+
tmpLabelElement.setAttribute('x', String(tmpTextX));
|
|
205
|
+
tmpLabelElement.setAttribute('y', String(tmpBadgeY + tmpBadgeHeight / 2));
|
|
206
|
+
tmpLabelElement.setAttribute('text-anchor', tmpTextAnchor);
|
|
207
|
+
tmpLabelElement.setAttribute('dominant-baseline', 'central');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Port circle (rendered on top of badge background)
|
|
211
|
+
let tmpShapeProvider = this._FlowView._ConnectorShapesProvider;
|
|
212
|
+
let tmpCircle;
|
|
213
|
+
if (tmpShapeProvider)
|
|
214
|
+
{
|
|
215
|
+
tmpCircle = tmpShapeProvider.createPortElement(tmpPort, tmpPosition, pNodeData.Hash);
|
|
216
|
+
}
|
|
217
|
+
else
|
|
218
|
+
{
|
|
219
|
+
tmpCircle = this._FlowView._SVGHelperProvider.createSVGElement('circle');
|
|
220
|
+
let tmpPortClass = `pict-flow-port ${tmpPort.Direction}`;
|
|
221
|
+
if (tmpPort.PortType)
|
|
222
|
+
{
|
|
223
|
+
tmpPortClass += ` port-type-${tmpPort.PortType}`;
|
|
224
|
+
}
|
|
225
|
+
tmpCircle.setAttribute('class', tmpPortClass);
|
|
226
|
+
tmpCircle.setAttribute('cx', String(tmpPosition.x));
|
|
227
|
+
tmpCircle.setAttribute('cy', String(tmpPosition.y));
|
|
228
|
+
tmpCircle.setAttribute('r', '5');
|
|
229
|
+
tmpCircle.setAttribute('data-port-hash', tmpPort.Hash);
|
|
230
|
+
tmpCircle.setAttribute('data-node-hash', pNodeData.Hash);
|
|
231
|
+
tmpCircle.setAttribute('data-port-direction', tmpPort.Direction);
|
|
232
|
+
if (tmpPort.PortType)
|
|
233
|
+
{
|
|
234
|
+
tmpCircle.setAttribute('data-port-type', tmpPort.PortType);
|
|
235
|
+
}
|
|
236
|
+
tmpCircle.setAttribute('data-element-type', 'port');
|
|
237
|
+
}
|
|
238
|
+
pGroup.appendChild(tmpCircle);
|
|
239
|
+
|
|
240
|
+
// Port label text (on top of everything)
|
|
241
|
+
if (tmpLabelElement)
|
|
242
|
+
{
|
|
243
|
+
pGroup.appendChild(tmpLabelElement);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Calculate port position relative to node origin.
|
|
251
|
+
*
|
|
252
|
+
* Delegates to the geometry provider's getPortLocalPosition method.
|
|
253
|
+
*
|
|
254
|
+
* @param {string} pSide - 'left', 'right', 'top', 'bottom' (or compound sides)
|
|
255
|
+
* @param {number} pIndex - Index of this port on its side
|
|
256
|
+
* @param {number} pTotal - Total ports on this side
|
|
257
|
+
* @param {number} pWidth - Node width
|
|
258
|
+
* @param {number} pHeight - Node height
|
|
259
|
+
* @param {number} pNodeTitleBarHeight - Title bar height
|
|
260
|
+
* @param {Object} [pPortCountsBySide] - Optional map of Side → count for adaptive zones
|
|
261
|
+
* @returns {{x: number, y: number}}
|
|
262
|
+
*/
|
|
263
|
+
getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, pNodeTitleBarHeight, pPortCountsBySide)
|
|
264
|
+
{
|
|
265
|
+
return this._FlowView._GeometryProvider.getPortLocalPosition(pSide, pIndex, pTotal, pWidth, pHeight, pNodeTitleBarHeight, pPortCountsBySide);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
module.exports = PictServiceFlowPortRenderer;
|