@zoneflow/renderer-dom 0.0.6 → 0.0.8

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.
@@ -5,6 +5,21 @@ const DEFAULT_DEBUG_LAYERS = [
5
5
  "edges",
6
6
  "anchors",
7
7
  ];
8
+ const EDGE_FLOW_CLASS = "zoneflow-debug-edge-flow";
9
+ const EDGE_FLOW_STYLE = `
10
+ @keyframes zoneflow-debug-edge-flow {
11
+ from { stroke-dashoffset: 18; }
12
+ to { stroke-dashoffset: 0; }
13
+ }
14
+ .${EDGE_FLOW_CLASS} {
15
+ animation: zoneflow-debug-edge-flow 900ms linear infinite;
16
+ }
17
+ @media (prefers-reduced-motion: reduce) {
18
+ .${EDGE_FLOW_CLASS} {
19
+ animation: none;
20
+ }
21
+ }
22
+ `;
8
23
  const drawLayerMap = {
9
24
  "graph-layout": drawGraphLayout,
10
25
  density: drawDensity,
@@ -17,6 +32,11 @@ const drawLayerMap = {
17
32
  function createSvgElement(tag) {
18
33
  return document.createElementNS("http://www.w3.org/2000/svg", tag);
19
34
  }
35
+ function appendEdgeFlowStyle(svg) {
36
+ const style = createSvgElement("style");
37
+ style.textContent = EDGE_FLOW_STYLE;
38
+ svg.appendChild(style);
39
+ }
20
40
  function sortZoneVisualsForRender(pipeline) {
21
41
  function getDepth(zoneId) {
22
42
  let depth = 0;
@@ -41,22 +61,39 @@ function getEdgeColor(kind) {
41
61
  }
42
62
  function getBezierCurvePathD(params) {
43
63
  const { source, target } = params;
64
+ const distanceX = Math.abs(target.x - source.x);
65
+ const distanceY = Math.abs(target.y - source.y);
66
+ if (distanceX <= 72 && distanceY <= 48) {
67
+ return `M ${source.x} ${source.y} L ${target.x} ${target.y}`;
68
+ }
44
69
  const sourceLead = Math.min(Math.max(Math.abs(target.x - source.x) * 0.18, 18), 42);
45
70
  const leadSourceX = source.x + sourceLead;
46
- const dx = target.x - leadSourceX;
47
- const direction = dx >= 0 ? 1 : -1;
71
+ const targetLead = Math.min(Math.max(Math.abs(target.x - source.x) * 0.16, 18), 42);
72
+ const targetApproachX = target.x - targetLead;
73
+ const shouldRouteAround = targetApproachX - leadSourceX < 36;
74
+ if (shouldRouteAround) {
75
+ const bridgeDistance = Math.abs(leadSourceX - targetApproachX);
76
+ const midX = (leadSourceX + targetApproachX) / 2;
77
+ const sourceBendX = leadSourceX + Math.min(Math.max(bridgeDistance * 0.22, 28), 72);
78
+ const targetBendX = targetApproachX - Math.min(Math.max(bridgeDistance * 0.22, 28), 72);
79
+ const verticalGap = Math.abs(target.y - source.y);
80
+ const verticalDirection = target.y >= source.y ? 1 : -1;
81
+ const laneOffset = Math.min(Math.max(Math.abs(target.x - source.x) * 0.22 + 48, 56), 144);
82
+ const laneY = (source.y + target.y) / 2 +
83
+ (verticalGap < 36 ? verticalDirection * laneOffset : 0);
84
+ return [
85
+ `M ${source.x} ${source.y}`,
86
+ `L ${leadSourceX} ${source.y}`,
87
+ `C ${sourceBendX} ${source.y}, ${sourceBendX} ${laneY}, ${midX} ${laneY}`,
88
+ `C ${targetBendX} ${laneY}, ${targetBendX} ${target.y}, ${targetApproachX} ${target.y}`,
89
+ `L ${target.x} ${target.y}`,
90
+ ].join(" ");
91
+ }
92
+ const dx = targetApproachX - leadSourceX;
48
93
  const handle = Math.min(Math.max(Math.abs(dx) * 0.45, 28), 104);
49
- const control1X = leadSourceX + handle * direction;
50
- const control2X = target.x - handle * direction;
51
- return `M ${source.x} ${source.y} L ${leadSourceX} ${source.y} C ${control1X} ${source.y}, ${control2X} ${target.y}, ${target.x} ${target.y}`;
52
- }
53
- function getChevronPathD(params) {
54
- const { target, direction } = params;
55
- const tipX = target.x - direction * 6;
56
- const baseX = tipX - direction * 7;
57
- const topY = target.y - 4;
58
- const bottomY = target.y + 4;
59
- return `M ${baseX} ${topY} L ${tipX} ${target.y} L ${baseX} ${bottomY}`;
94
+ const control1X = leadSourceX + handle;
95
+ const control2X = targetApproachX - handle;
96
+ return `M ${source.x} ${source.y} L ${leadSourceX} ${source.y} C ${control1X} ${source.y}, ${control2X} ${target.y}, ${targetApproachX} ${target.y} L ${target.x} ${target.y}`;
60
97
  }
61
98
  function filterPipelineForExclusion(input) {
62
99
  const excludedZoneIds = new Set(input.exclusionState?.excludedZoneIds ?? []);
@@ -266,32 +303,35 @@ function drawEdges(root, pipeline) {
266
303
  svg.style.height = "100%";
267
304
  svg.style.overflow = "visible";
268
305
  svg.style.pointerEvents = "none";
306
+ appendEdgeFlowStyle(svg);
269
307
  Object.values(edgesByPathId)
270
308
  .flatMap((edges) => edges)
271
309
  .forEach((edge) => {
272
310
  const path = createSvgElement("path");
273
311
  const stroke = getEdgeColor(edge.kind);
274
- path.setAttribute("d", getBezierCurvePathD({
312
+ const pathD = getBezierCurvePathD({
275
313
  source: edge.source,
276
314
  target: edge.target,
277
- }));
315
+ });
316
+ path.setAttribute("d", pathD);
278
317
  path.setAttribute("fill", "none");
279
318
  path.setAttribute("stroke", stroke);
280
319
  path.setAttribute("stroke-width", edge.kind === "zone-to-path" ? "2" : "2.4");
281
320
  path.setAttribute("stroke-linecap", "round");
282
321
  path.setAttribute("stroke-linejoin", "round");
322
+ path.setAttribute("opacity", "0.42");
283
323
  svg.appendChild(path);
284
- const chevron = createSvgElement("path");
285
- chevron.setAttribute("d", getChevronPathD({
286
- target: edge.target,
287
- direction: edge.kind === "path-to-zone" ? 1 : edge.target.x >= edge.source.x ? 1 : -1,
288
- }));
289
- chevron.setAttribute("fill", "none");
290
- chevron.setAttribute("stroke", stroke);
291
- chevron.setAttribute("stroke-width", edge.kind === "zone-to-path" ? "1.7" : "1.95");
292
- chevron.setAttribute("stroke-linecap", "round");
293
- chevron.setAttribute("stroke-linejoin", "round");
294
- svg.appendChild(chevron);
324
+ const flow = createSvgElement("path");
325
+ flow.setAttribute("d", pathD);
326
+ flow.setAttribute("fill", "none");
327
+ flow.setAttribute("stroke", stroke);
328
+ flow.setAttribute("stroke-width", edge.kind === "zone-to-path" ? "2.45" : "2.85");
329
+ flow.setAttribute("stroke-linecap", "round");
330
+ flow.setAttribute("stroke-linejoin", "round");
331
+ flow.setAttribute("stroke-dasharray", "1 11");
332
+ flow.setAttribute("stroke-dashoffset", "0");
333
+ flow.setAttribute("class", EDGE_FLOW_CLASS);
334
+ svg.appendChild(flow);
295
335
  });
296
336
  root.appendChild(svg);
297
337
  }
@@ -9,6 +9,21 @@ const RENDER_Z_INDEX = {
9
9
  edgeLayer: 20,
10
10
  pathLayer: 30,
11
11
  };
12
+ const EDGE_FLOW_CLASS = "zoneflow-edge-flow";
13
+ const EDGE_FLOW_STYLE = `
14
+ @keyframes zoneflow-edge-flow {
15
+ from { stroke-dashoffset: 18; }
16
+ to { stroke-dashoffset: 0; }
17
+ }
18
+ .${EDGE_FLOW_CLASS} {
19
+ animation: zoneflow-edge-flow 900ms linear infinite;
20
+ }
21
+ @media (prefers-reduced-motion: reduce) {
22
+ .${EDGE_FLOW_CLASS} {
23
+ animation: none;
24
+ }
25
+ }
26
+ `;
12
27
  function applyStyles(el, styles) {
13
28
  for (const [key, value] of Object.entries(styles)) {
14
29
  // @ts-expect-error CSSStyleDeclaration index access
@@ -34,10 +49,21 @@ function sortZonesForRender(params) {
34
49
  zone,
35
50
  index,
36
51
  depth: getZoneDepth(params.input.model, zone.zoneId),
52
+ zOrder: params.input.layoutModel.zoneLayoutsById[zone.zoneId]?.zOrder ?? index,
37
53
  }))
38
- .sort((a, b) => a.depth - b.depth || a.index - b.index)
54
+ .sort((a, b) => a.depth - b.depth || a.zOrder - b.zOrder || a.index - b.index)
39
55
  .map((entry) => entry.zone);
40
56
  }
57
+ function sortPathsForRender(params) {
58
+ return Object.values(params.pathsById)
59
+ .map((path, index) => ({
60
+ path,
61
+ index,
62
+ zOrder: params.input.layoutModel.pathLayoutsById[path.pathId]?.zOrder ?? index,
63
+ }))
64
+ .sort((a, b) => a.zOrder - b.zOrder || a.index - b.index)
65
+ .map((entry) => entry.path);
66
+ }
41
67
  function resolvePathDisplayName(params) {
42
68
  const trimmed = params.name.trim();
43
69
  if (trimmed)
@@ -110,6 +136,11 @@ function getOpacity(emphasis) {
110
136
  function createSvgElement(tag) {
111
137
  return document.createElementNS("http://www.w3.org/2000/svg", tag);
112
138
  }
139
+ function appendEdgeFlowStyle(svg) {
140
+ const style = createSvgElement("style");
141
+ style.textContent = EDGE_FLOW_STYLE;
142
+ svg.appendChild(style);
143
+ }
113
144
  function getEdgeColor(params) {
114
145
  return params.kind === "zone-to-path"
115
146
  ? params.theme.pathEdge
@@ -117,22 +148,39 @@ function getEdgeColor(params) {
117
148
  }
118
149
  function getBezierCurvePathD(params) {
119
150
  const { source, target } = params;
151
+ const distanceX = Math.abs(target.x - source.x);
152
+ const distanceY = Math.abs(target.y - source.y);
153
+ if (distanceX <= 72 && distanceY <= 48) {
154
+ return `M ${source.x} ${source.y} L ${target.x} ${target.y}`;
155
+ }
120
156
  const sourceLead = Math.min(Math.max(Math.abs(target.x - source.x) * 0.18, 18), 42);
121
157
  const leadSourceX = source.x + sourceLead;
122
- const dx = target.x - leadSourceX;
123
- const direction = dx >= 0 ? 1 : -1;
158
+ const targetLead = Math.min(Math.max(Math.abs(target.x - source.x) * 0.16, 18), 42);
159
+ const targetApproachX = target.x - targetLead;
160
+ const shouldRouteAround = targetApproachX - leadSourceX < 36;
161
+ if (shouldRouteAround) {
162
+ const bridgeDistance = Math.abs(leadSourceX - targetApproachX);
163
+ const midX = (leadSourceX + targetApproachX) / 2;
164
+ const sourceBendX = leadSourceX + Math.min(Math.max(bridgeDistance * 0.22, 28), 72);
165
+ const targetBendX = targetApproachX - Math.min(Math.max(bridgeDistance * 0.22, 28), 72);
166
+ const verticalGap = Math.abs(target.y - source.y);
167
+ const verticalDirection = target.y >= source.y ? 1 : -1;
168
+ const laneOffset = Math.min(Math.max(Math.abs(target.x - source.x) * 0.22 + 48, 56), 144);
169
+ const laneY = (source.y + target.y) / 2 +
170
+ (verticalGap < 36 ? verticalDirection * laneOffset : 0);
171
+ return [
172
+ `M ${source.x} ${source.y}`,
173
+ `L ${leadSourceX} ${source.y}`,
174
+ `C ${sourceBendX} ${source.y}, ${sourceBendX} ${laneY}, ${midX} ${laneY}`,
175
+ `C ${targetBendX} ${laneY}, ${targetBendX} ${target.y}, ${targetApproachX} ${target.y}`,
176
+ `L ${target.x} ${target.y}`,
177
+ ].join(" ");
178
+ }
179
+ const dx = targetApproachX - leadSourceX;
124
180
  const handle = Math.min(Math.max(Math.abs(dx) * 0.45, 28), 104);
125
- const control1X = leadSourceX + handle * direction;
126
- const control2X = target.x - handle * direction;
127
- return `M ${source.x} ${source.y} L ${leadSourceX} ${source.y} C ${control1X} ${source.y}, ${control2X} ${target.y}, ${target.x} ${target.y}`;
128
- }
129
- function getChevronPathD(params) {
130
- const { target, direction } = params;
131
- const tipX = target.x - direction * 6;
132
- const baseX = tipX - direction * 7;
133
- const topY = target.y - 4;
134
- const bottomY = target.y + 4;
135
- return `M ${baseX} ${topY} L ${tipX} ${target.y} L ${baseX} ${bottomY}`;
181
+ const control1X = leadSourceX + handle;
182
+ const control2X = targetApproachX - handle;
183
+ return `M ${source.x} ${source.y} L ${leadSourceX} ${source.y} C ${control1X} ${source.y}, ${control2X} ${target.y}, ${targetApproachX} ${target.y} L ${target.x} ${target.y}`;
136
184
  }
137
185
  function computeSceneBounds(input) {
138
186
  const { pipeline, viewportInfo, } = input;
@@ -412,6 +460,7 @@ function createPathSlotHost(params) {
412
460
  }
413
461
  function drawEdges(params) {
414
462
  const { svg, input } = params;
463
+ appendEdgeFlowStyle(svg);
415
464
  for (const [pathId, edges] of Object.entries(input.pipeline.graphLayout.edgesByPathId)) {
416
465
  const visibility = input.pipeline.visibility.pathVisibilityById[pathId];
417
466
  if (!visibility?.shouldRenderEdge)
@@ -421,30 +470,32 @@ function drawEdges(params) {
421
470
  kind: edge.kind,
422
471
  theme: input.theme,
423
472
  });
424
- const path = createSvgElement("path");
425
- path.setAttribute("d", getBezierCurvePathD({
473
+ const pathD = getBezierCurvePathD({
426
474
  source: edge.source,
427
475
  target: edge.target,
428
- }));
476
+ });
477
+ const opacity = getOpacity(visibility.emphasis);
478
+ const path = createSvgElement("path");
479
+ path.setAttribute("d", pathD);
429
480
  path.setAttribute("fill", "none");
430
481
  path.setAttribute("stroke", stroke);
431
482
  path.setAttribute("stroke-width", edge.kind === "path-to-zone" ? "2.25" : "1.85");
432
483
  path.setAttribute("stroke-linecap", "round");
433
484
  path.setAttribute("stroke-linejoin", "round");
434
- path.setAttribute("opacity", String(getOpacity(visibility.emphasis)));
485
+ path.setAttribute("opacity", String(opacity * 0.42));
435
486
  svg.appendChild(path);
436
- const chevron = createSvgElement("path");
437
- chevron.setAttribute("d", getChevronPathD({
438
- target: edge.target,
439
- direction: edge.kind === "path-to-zone" ? 1 : edge.target.x >= edge.source.x ? 1 : -1,
440
- }));
441
- chevron.setAttribute("fill", "none");
442
- chevron.setAttribute("stroke", stroke);
443
- chevron.setAttribute("stroke-width", edge.kind === "path-to-zone" ? "1.95" : "1.7");
444
- chevron.setAttribute("stroke-linecap", "round");
445
- chevron.setAttribute("stroke-linejoin", "round");
446
- chevron.setAttribute("opacity", String(getOpacity(visibility.emphasis)));
447
- svg.appendChild(chevron);
487
+ const flow = createSvgElement("path");
488
+ flow.setAttribute("d", pathD);
489
+ flow.setAttribute("fill", "none");
490
+ flow.setAttribute("stroke", stroke);
491
+ flow.setAttribute("stroke-width", edge.kind === "path-to-zone" ? "2.8" : "2.35");
492
+ flow.setAttribute("stroke-linecap", "round");
493
+ flow.setAttribute("stroke-linejoin", "round");
494
+ flow.setAttribute("stroke-dasharray", "1 11");
495
+ flow.setAttribute("stroke-dashoffset", "0");
496
+ flow.setAttribute("opacity", String(opacity));
497
+ flow.setAttribute("class", EDGE_FLOW_CLASS);
498
+ svg.appendChild(flow);
448
499
  }
449
500
  }
450
501
  }
@@ -701,7 +752,10 @@ export const domDrawEngine = {
701
752
  });
702
753
  zoneLayer.appendChild(zoneEl);
703
754
  }
704
- for (const pathVisual of Object.values(pipeline.graphLayout.pathsById)) {
755
+ for (const pathVisual of sortPathsForRender({
756
+ input,
757
+ pathsById: pipeline.graphLayout.pathsById,
758
+ })) {
705
759
  const visibility = pipeline.visibility.pathVisibilityById[pathVisual.pathId];
706
760
  if (!visibility?.shouldRenderNode ||
707
761
  !pathVisual.rect ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zoneflow/renderer-dom",
3
- "version": "0.0.6",
3
+ "version": "0.0.8",
4
4
  "license": "MIT",
5
5
  "description": "Low-level DOM renderer engines for Zoneflow.",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  "dist"
20
20
  ],
21
21
  "dependencies": {
22
- "@zoneflow/core": "0.0.6"
22
+ "@zoneflow/core": "0.0.8"
23
23
  },
24
24
  "scripts": {
25
25
  "build": "tsc -p tsconfig.json",