@transitrix/diagrams 1.8.18 → 1.8.19
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/dist/edge-path.d.ts
CHANGED
|
@@ -20,3 +20,18 @@ export declare const DEFAULT_EDGE_CURVATURE = 1;
|
|
|
20
20
|
* - >1 → progressively stronger arc at that end.
|
|
21
21
|
*/
|
|
22
22
|
export declare function horizontalCubicEdgePath(sx: number, sy: number, tx: number, ty: number, curvature?: number, entryCurvature?: number): string;
|
|
23
|
+
/**
|
|
24
|
+
* Builds the SVG `d` for a "skip" edge that must arc over an intermediate
|
|
25
|
+
* node sitting on the direct (sx,sy)→(tx,ty) line — e.g. a Network/PSND view
|
|
26
|
+
* where a multi-predecessor activity's nearer predecessor shares a row with
|
|
27
|
+
* one of the columns in between, so the direct line would be drawn straight
|
|
28
|
+
* through that node. Both control points target `bowY` instead of each
|
|
29
|
+
* endpoint's own Y, producing a hump that departs/arrives at an angle rather
|
|
30
|
+
* than the flat horizontal tangent of `horizontalCubicEdgePath`.
|
|
31
|
+
*
|
|
32
|
+
* A cubic's actual apex only reaches part-way from an endpoint Y to the
|
|
33
|
+
* control-point Y (75% at the midpoint for a fully symmetric sy=ty curve),
|
|
34
|
+
* so callers must pick `bowY` well past the obstacle for the apex to clear
|
|
35
|
+
* it — see `render-activities.ts`'s `blockingNodes`/`bowY` for the margin.
|
|
36
|
+
*/
|
|
37
|
+
export declare function bowedCubicEdgePath(sx: number, sy: number, tx: number, ty: number, bowY: number, curvature?: number): string;
|
package/dist/edge-path.js
CHANGED
|
@@ -47,3 +47,22 @@ export function horizontalCubicEdgePath(sx, sy, tx, ty, curvature = DEFAULT_EDGE
|
|
|
47
47
|
const entryHandle = baseHandle * (entryCurvature ?? curvature);
|
|
48
48
|
return `M${sx},${sy} C${sx + exitHandle},${sy} ${tx - entryHandle},${ty} ${tx},${ty}`;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Builds the SVG `d` for a "skip" edge that must arc over an intermediate
|
|
52
|
+
* node sitting on the direct (sx,sy)→(tx,ty) line — e.g. a Network/PSND view
|
|
53
|
+
* where a multi-predecessor activity's nearer predecessor shares a row with
|
|
54
|
+
* one of the columns in between, so the direct line would be drawn straight
|
|
55
|
+
* through that node. Both control points target `bowY` instead of each
|
|
56
|
+
* endpoint's own Y, producing a hump that departs/arrives at an angle rather
|
|
57
|
+
* than the flat horizontal tangent of `horizontalCubicEdgePath`.
|
|
58
|
+
*
|
|
59
|
+
* A cubic's actual apex only reaches part-way from an endpoint Y to the
|
|
60
|
+
* control-point Y (75% at the midpoint for a fully symmetric sy=ty curve),
|
|
61
|
+
* so callers must pick `bowY` well past the obstacle for the apex to clear
|
|
62
|
+
* it — see `render-activities.ts`'s `blockingNodes`/`bowY` for the margin.
|
|
63
|
+
*/
|
|
64
|
+
export function bowedCubicEdgePath(sx, sy, tx, ty, bowY, curvature = DEFAULT_EDGE_CURVATURE) {
|
|
65
|
+
const dx = tx - sx;
|
|
66
|
+
const handle = Math.max(EDGE_MIN_HANDLE, Math.min(Math.abs(dx) * DX_FACTOR, MAX_HANDLE)) * curvature;
|
|
67
|
+
return `M${sx},${sy} C${sx + handle},${bowY} ${tx - handle},${bowY} ${tx},${ty}`;
|
|
68
|
+
}
|
|
@@ -10,6 +10,15 @@ import { type NodeSizePreset } from '../node-size-presets.js';
|
|
|
10
10
|
export declare const ACTIVITIES_NETWORK_CSS = "\n .act-node { fill: var(--ts-layer-activity, #d4edda); stroke: var(--ts-node-stroke, #004d67); stroke-width: 1; }\n .critical-node { fill: var(--ts-brand-orange-tint, #ffeee5); stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 1; }\n .milestone-node { fill: #ecfeff; stroke: var(--ts-text-muted, #64748b); stroke-dasharray: 4 2; }\n .critical-edge { stroke: var(--ts-brand-orange, #ff4d00); stroke-width: 1.5; }\n .arrow-fill-critical { fill: var(--ts-brand-orange, #ff4d00); }\n";
|
|
11
11
|
/** Arrowhead marker defs shared by both hosts for the network view. */
|
|
12
12
|
export declare const ACTIVITIES_NETWORK_DEFS = "<defs>\n <marker id=\"arrow\" markerWidth=\"8\" markerHeight=\"8\" refX=\"8\" refY=\"3\" orient=\"auto\">\n <path d=\"M0,0 L0,6 L8,3 z\" class=\"arrow-fill\"/>\n </marker>\n <marker id=\"arrow-crit\" markerWidth=\"8\" markerHeight=\"8\" refX=\"8\" refY=\"3\" orient=\"auto\">\n <path d=\"M0,0 L0,6 L8,3 z\" class=\"arrow-fill-critical\"/>\n </marker>\n</defs>";
|
|
13
|
+
/**
|
|
14
|
+
* Extra top padding (px) a network-view canvas must add so skip-edge bows
|
|
15
|
+
* (see `blockingNodes`/`bowY` above) never draw above the SVG's y=0 and get
|
|
16
|
+
* clipped. `oyBase` is the canvas's Y offset *before* this padding — the
|
|
17
|
+
* historical `-bounds.y + N_PAD + titleH` both hosts compute — since a bow
|
|
18
|
+
* near the topmost row can reach above the diagram's own node bounds.
|
|
19
|
+
* Callers add the returned value to both `oyBase` and the canvas height.
|
|
20
|
+
*/
|
|
21
|
+
export declare function computeNetworkTopPad(layout: ActivitiesLayout, oyBase: number): number;
|
|
13
22
|
/**
|
|
14
23
|
* Canonical Network (PSND) body — the node rects and edge paths that go inside
|
|
15
24
|
* the `<svg>`, shared verbatim by the VS Code preview and the host-neutral
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { layoutActivities } from '../activities/layout.js';
|
|
20
20
|
import { computeCpm } from '../activities/cpm.js';
|
|
21
|
-
import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
21
|
+
import { horizontalCubicEdgePath, bowedCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
22
22
|
import { parseNodeSizePreset, resolveActionNodeSize } from '../node-size-presets.js';
|
|
23
23
|
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
24
24
|
import { emitCenteredTextSvg, layoutCenteredEntityText } from './entity-text-layout.js';
|
|
@@ -46,6 +46,68 @@ export const ACTIVITIES_NETWORK_DEFS = `<defs>
|
|
|
46
46
|
<path d="M0,0 L0,6 L8,3 z" class="arrow-fill-critical"/>
|
|
47
47
|
</marker>
|
|
48
48
|
</defs>`;
|
|
49
|
+
/**
|
|
50
|
+
* Nodes horizontally between `source` and `target` (a "skip" edge passes over
|
|
51
|
+
* their column) whose vertical span overlaps the edge's Y band. A
|
|
52
|
+
* straight-tangent cubic (`horizontalCubicEdgePath`) stays within
|
|
53
|
+
* [min(sourceCenterY,targetCenterY), max(...)] by convex hull, so any such
|
|
54
|
+
* node sits directly on the edge's path — the "arrow goes straight through
|
|
55
|
+
* the node" bug reported against a linear PSND chain where a multi-predecessor
|
|
56
|
+
* activity's nearer predecessor shares a row with an intermediate column.
|
|
57
|
+
*/
|
|
58
|
+
function blockingNodes(nodes, source, target) {
|
|
59
|
+
const sCenterY = source.y + source.height / 2;
|
|
60
|
+
const tCenterY = target.y + target.height / 2;
|
|
61
|
+
const loY = Math.min(sCenterY, tCenterY);
|
|
62
|
+
const hiY = Math.max(sCenterY, tCenterY);
|
|
63
|
+
const sRight = source.x + source.width;
|
|
64
|
+
const tLeft = target.x;
|
|
65
|
+
return nodes.filter((n) => {
|
|
66
|
+
if (n.id === source.id || n.id === target.id)
|
|
67
|
+
return false;
|
|
68
|
+
if (n.x + n.width <= sRight || n.x >= tLeft)
|
|
69
|
+
return false; // not between the two columns
|
|
70
|
+
return n.y < hiY && n.y + n.height > loY; // vertical overlap with the edge's band
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Pre-offset bow-control-point Y for a skip edge arcing over `blockers`: one
|
|
75
|
+
* full node-height of clearance above the topmost blocker's top edge. A
|
|
76
|
+
* cubic's apex only reaches part-way toward its control point (see
|
|
77
|
+
* `bowedCubicEdgePath` doc), so this generous margin is what actually keeps
|
|
78
|
+
* the visible curve clear of the box — verified numerically in
|
|
79
|
+
* `render-activities.test.ts`.
|
|
80
|
+
*/
|
|
81
|
+
function bowY(blockers) {
|
|
82
|
+
const topY = Math.min(...blockers.map((n) => n.y));
|
|
83
|
+
const clearance = Math.max(...blockers.map((n) => n.height));
|
|
84
|
+
return topY - clearance;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Extra top padding (px) a network-view canvas must add so skip-edge bows
|
|
88
|
+
* (see `blockingNodes`/`bowY` above) never draw above the SVG's y=0 and get
|
|
89
|
+
* clipped. `oyBase` is the canvas's Y offset *before* this padding — the
|
|
90
|
+
* historical `-bounds.y + N_PAD + titleH` both hosts compute — since a bow
|
|
91
|
+
* near the topmost row can reach above the diagram's own node bounds.
|
|
92
|
+
* Callers add the returned value to both `oyBase` and the canvas height.
|
|
93
|
+
*/
|
|
94
|
+
export function computeNetworkTopPad(layout, oyBase) {
|
|
95
|
+
const nodeMap = new Map(layout.nodes.map((n) => [n.id, n]));
|
|
96
|
+
let pad = 0;
|
|
97
|
+
for (const e of layout.edges) {
|
|
98
|
+
const s = nodeMap.get(e.sourceId);
|
|
99
|
+
const t = nodeMap.get(e.targetId);
|
|
100
|
+
if (!s || !t)
|
|
101
|
+
continue;
|
|
102
|
+
const blockers = blockingNodes(layout.nodes, s, t);
|
|
103
|
+
if (blockers.length === 0)
|
|
104
|
+
continue;
|
|
105
|
+
const shortfall = -(oyBase + bowY(blockers));
|
|
106
|
+
if (shortfall > pad)
|
|
107
|
+
pad = shortfall;
|
|
108
|
+
}
|
|
109
|
+
return pad;
|
|
110
|
+
}
|
|
49
111
|
function activityNodeSvg(n, x, y, isCritical, isMilestone, durLabel) {
|
|
50
112
|
const cls = `diagram-node act-node ${isCritical ? 'critical-node' : ''} ${isMilestone ? 'milestone-node' : ''}`.trim();
|
|
51
113
|
const specs = layoutCenteredEntityText({
|
|
@@ -92,7 +154,11 @@ export function renderActivitiesNetworkBody(layout, cpm, ox, oy, curvature, entr
|
|
|
92
154
|
const ty = t.y + oy + t.height / 2;
|
|
93
155
|
const cls = e.isCritical ? 'diagram-edge critical-edge' : 'diagram-edge';
|
|
94
156
|
const marker = `url(#${e.isCritical ? 'arrow-crit' : 'arrow'})`;
|
|
95
|
-
|
|
157
|
+
const blockers = blockingNodes(layout.nodes, s, t);
|
|
158
|
+
const d = blockers.length > 0
|
|
159
|
+
? bowedCubicEdgePath(sx, sy, tx, ty, bowY(blockers) + oy, curvature)
|
|
160
|
+
: horizontalCubicEdgePath(sx, sy, tx, ty, curvature, entryCurvature);
|
|
161
|
+
return `<path d="${d}" class="${cls}" marker-end="${marker}"/>`;
|
|
96
162
|
})
|
|
97
163
|
.join('\n');
|
|
98
164
|
const nodeSvg = layout.nodes
|
|
@@ -142,10 +208,12 @@ export function renderActivitiesSvg(doc, options = {}) {
|
|
|
142
208
|
}
|
|
143
209
|
const cpm = computeCpm(renderDoc.activities ?? []);
|
|
144
210
|
const titleH = title ? 24 : 0;
|
|
211
|
+
const oyBase = -layout.bounds.y + N_PAD + titleH;
|
|
212
|
+
const topPad = computeNetworkTopPad(layout, oyBase);
|
|
145
213
|
const w = layout.bounds.width + N_PAD * 2;
|
|
146
|
-
const h = layout.bounds.height + N_PAD * 2 + titleH;
|
|
214
|
+
const h = layout.bounds.height + N_PAD * 2 + titleH + topPad;
|
|
147
215
|
const ox = -layout.bounds.x + N_PAD;
|
|
148
|
-
const oy =
|
|
216
|
+
const oy = oyBase + topPad;
|
|
149
217
|
const body = renderActivitiesNetworkBody(layout, cpm, ox, oy, curvature, entryCurvature);
|
|
150
218
|
const titleSvg = title
|
|
151
219
|
? `<text class="text-header" x="${N_PAD}" y="${N_PAD - 6}">${escXml(title)}</text>`
|
package/package.json
CHANGED
package/src/edge-path.ts
CHANGED
|
@@ -62,3 +62,30 @@ export function horizontalCubicEdgePath(
|
|
|
62
62
|
const entryHandle = baseHandle * (entryCurvature ?? curvature);
|
|
63
63
|
return `M${sx},${sy} C${sx + exitHandle},${sy} ${tx - entryHandle},${ty} ${tx},${ty}`;
|
|
64
64
|
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Builds the SVG `d` for a "skip" edge that must arc over an intermediate
|
|
68
|
+
* node sitting on the direct (sx,sy)→(tx,ty) line — e.g. a Network/PSND view
|
|
69
|
+
* where a multi-predecessor activity's nearer predecessor shares a row with
|
|
70
|
+
* one of the columns in between, so the direct line would be drawn straight
|
|
71
|
+
* through that node. Both control points target `bowY` instead of each
|
|
72
|
+
* endpoint's own Y, producing a hump that departs/arrives at an angle rather
|
|
73
|
+
* than the flat horizontal tangent of `horizontalCubicEdgePath`.
|
|
74
|
+
*
|
|
75
|
+
* A cubic's actual apex only reaches part-way from an endpoint Y to the
|
|
76
|
+
* control-point Y (75% at the midpoint for a fully symmetric sy=ty curve),
|
|
77
|
+
* so callers must pick `bowY` well past the obstacle for the apex to clear
|
|
78
|
+
* it — see `render-activities.ts`'s `blockingNodes`/`bowY` for the margin.
|
|
79
|
+
*/
|
|
80
|
+
export function bowedCubicEdgePath(
|
|
81
|
+
sx: number,
|
|
82
|
+
sy: number,
|
|
83
|
+
tx: number,
|
|
84
|
+
ty: number,
|
|
85
|
+
bowY: number,
|
|
86
|
+
curvature: number = DEFAULT_EDGE_CURVATURE,
|
|
87
|
+
): string {
|
|
88
|
+
const dx = tx - sx;
|
|
89
|
+
const handle = Math.max(EDGE_MIN_HANDLE, Math.min(Math.abs(dx) * DX_FACTOR, MAX_HANDLE)) * curvature;
|
|
90
|
+
return `M${sx},${sy} C${sx + handle},${bowY} ${tx - handle},${bowY} ${tx},${ty}`;
|
|
91
|
+
}
|
|
@@ -23,7 +23,7 @@ import type {
|
|
|
23
23
|
ActivitiesLayout,
|
|
24
24
|
ActivitiesLayoutOptions,
|
|
25
25
|
} from '../activities/types.js';
|
|
26
|
-
import { horizontalCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
26
|
+
import { horizontalCubicEdgePath, bowedCubicEdgePath, DEFAULT_EDGE_CURVATURE } from '../edge-path.js';
|
|
27
27
|
import { parseNodeSizePreset, resolveActionNodeSize, type NodeSizePreset } from '../node-size-presets.js';
|
|
28
28
|
import { generateSvgEmbedCss } from '../theme/index.js';
|
|
29
29
|
import { emitCenteredTextSvg, layoutCenteredEntityText } from './entity-text-layout.js';
|
|
@@ -55,6 +55,72 @@ export const ACTIVITIES_NETWORK_DEFS = `<defs>
|
|
|
55
55
|
</marker>
|
|
56
56
|
</defs>`;
|
|
57
57
|
|
|
58
|
+
type ActivityLayoutNode = ActivitiesLayout['nodes'][number];
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Nodes horizontally between `source` and `target` (a "skip" edge passes over
|
|
62
|
+
* their column) whose vertical span overlaps the edge's Y band. A
|
|
63
|
+
* straight-tangent cubic (`horizontalCubicEdgePath`) stays within
|
|
64
|
+
* [min(sourceCenterY,targetCenterY), max(...)] by convex hull, so any such
|
|
65
|
+
* node sits directly on the edge's path — the "arrow goes straight through
|
|
66
|
+
* the node" bug reported against a linear PSND chain where a multi-predecessor
|
|
67
|
+
* activity's nearer predecessor shares a row with an intermediate column.
|
|
68
|
+
*/
|
|
69
|
+
function blockingNodes(
|
|
70
|
+
nodes: ActivityLayoutNode[],
|
|
71
|
+
source: ActivityLayoutNode,
|
|
72
|
+
target: ActivityLayoutNode,
|
|
73
|
+
): ActivityLayoutNode[] {
|
|
74
|
+
const sCenterY = source.y + source.height / 2;
|
|
75
|
+
const tCenterY = target.y + target.height / 2;
|
|
76
|
+
const loY = Math.min(sCenterY, tCenterY);
|
|
77
|
+
const hiY = Math.max(sCenterY, tCenterY);
|
|
78
|
+
const sRight = source.x + source.width;
|
|
79
|
+
const tLeft = target.x;
|
|
80
|
+
return nodes.filter((n) => {
|
|
81
|
+
if (n.id === source.id || n.id === target.id) return false;
|
|
82
|
+
if (n.x + n.width <= sRight || n.x >= tLeft) return false; // not between the two columns
|
|
83
|
+
return n.y < hiY && n.y + n.height > loY; // vertical overlap with the edge's band
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Pre-offset bow-control-point Y for a skip edge arcing over `blockers`: one
|
|
89
|
+
* full node-height of clearance above the topmost blocker's top edge. A
|
|
90
|
+
* cubic's apex only reaches part-way toward its control point (see
|
|
91
|
+
* `bowedCubicEdgePath` doc), so this generous margin is what actually keeps
|
|
92
|
+
* the visible curve clear of the box — verified numerically in
|
|
93
|
+
* `render-activities.test.ts`.
|
|
94
|
+
*/
|
|
95
|
+
function bowY(blockers: ActivityLayoutNode[]): number {
|
|
96
|
+
const topY = Math.min(...blockers.map((n) => n.y));
|
|
97
|
+
const clearance = Math.max(...blockers.map((n) => n.height));
|
|
98
|
+
return topY - clearance;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Extra top padding (px) a network-view canvas must add so skip-edge bows
|
|
103
|
+
* (see `blockingNodes`/`bowY` above) never draw above the SVG's y=0 and get
|
|
104
|
+
* clipped. `oyBase` is the canvas's Y offset *before* this padding — the
|
|
105
|
+
* historical `-bounds.y + N_PAD + titleH` both hosts compute — since a bow
|
|
106
|
+
* near the topmost row can reach above the diagram's own node bounds.
|
|
107
|
+
* Callers add the returned value to both `oyBase` and the canvas height.
|
|
108
|
+
*/
|
|
109
|
+
export function computeNetworkTopPad(layout: ActivitiesLayout, oyBase: number): number {
|
|
110
|
+
const nodeMap = new Map(layout.nodes.map((n) => [n.id, n]));
|
|
111
|
+
let pad = 0;
|
|
112
|
+
for (const e of layout.edges) {
|
|
113
|
+
const s = nodeMap.get(e.sourceId);
|
|
114
|
+
const t = nodeMap.get(e.targetId);
|
|
115
|
+
if (!s || !t) continue;
|
|
116
|
+
const blockers = blockingNodes(layout.nodes, s, t);
|
|
117
|
+
if (blockers.length === 0) continue;
|
|
118
|
+
const shortfall = -(oyBase + bowY(blockers));
|
|
119
|
+
if (shortfall > pad) pad = shortfall;
|
|
120
|
+
}
|
|
121
|
+
return pad;
|
|
122
|
+
}
|
|
123
|
+
|
|
58
124
|
function activityNodeSvg(
|
|
59
125
|
n: ActivitiesLayout['nodes'][number],
|
|
60
126
|
x: number,
|
|
@@ -119,7 +185,11 @@ export function renderActivitiesNetworkBody(
|
|
|
119
185
|
const ty = t.y + oy + t.height / 2;
|
|
120
186
|
const cls = e.isCritical ? 'diagram-edge critical-edge' : 'diagram-edge';
|
|
121
187
|
const marker = `url(#${e.isCritical ? 'arrow-crit' : 'arrow'})`;
|
|
122
|
-
|
|
188
|
+
const blockers = blockingNodes(layout.nodes, s, t);
|
|
189
|
+
const d = blockers.length > 0
|
|
190
|
+
? bowedCubicEdgePath(sx, sy, tx, ty, bowY(blockers) + oy, curvature)
|
|
191
|
+
: horizontalCubicEdgePath(sx, sy, tx, ty, curvature, entryCurvature);
|
|
192
|
+
return `<path d="${d}" class="${cls}" marker-end="${marker}"/>`;
|
|
123
193
|
})
|
|
124
194
|
.join('\n');
|
|
125
195
|
|
|
@@ -207,10 +277,12 @@ export function renderActivitiesSvg(doc: ActivityDoc, options: RenderActivitiesO
|
|
|
207
277
|
|
|
208
278
|
const cpm = computeCpm(renderDoc.activities ?? []);
|
|
209
279
|
const titleH = title ? 24 : 0;
|
|
280
|
+
const oyBase = -layout.bounds.y + N_PAD + titleH;
|
|
281
|
+
const topPad = computeNetworkTopPad(layout, oyBase);
|
|
210
282
|
const w = layout.bounds.width + N_PAD * 2;
|
|
211
|
-
const h = layout.bounds.height + N_PAD * 2 + titleH;
|
|
283
|
+
const h = layout.bounds.height + N_PAD * 2 + titleH + topPad;
|
|
212
284
|
const ox = -layout.bounds.x + N_PAD;
|
|
213
|
-
const oy =
|
|
285
|
+
const oy = oyBase + topPad;
|
|
214
286
|
|
|
215
287
|
const body = renderActivitiesNetworkBody(layout, cpm, ox, oy, curvature, entryCurvature);
|
|
216
288
|
|