@jbrowse/bandage-core 11.7.3 → 11.8.0

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.
Files changed (99) hide show
  1. package/README.md +59 -19
  2. package/dist/bubbles/bubbleHalos.js +6 -6
  3. package/dist/bubbles/classifyBubble.js +5 -5
  4. package/dist/bubbles/superbubbles.js +7 -7
  5. package/dist/chunks/{chunk-ZZZ6NVLW.js → chunk-22ZMVREZ.js} +1 -1
  6. package/dist/chunks/{chunk-VXKL2ZLR.js → chunk-2H3ALI6L.js} +223 -45
  7. package/dist/chunks/{chunk-F4CDC72I.js → chunk-2RCKHCF7.js} +1 -1
  8. package/dist/chunks/{chunk-P276TWPM.js → chunk-3CXZUC2Y.js} +1 -1
  9. package/dist/chunks/{chunk-VNF236GG.js → chunk-3XYLEK4H.js} +2 -2
  10. package/dist/chunks/{chunk-PKGPSTRW.js → chunk-4OVIUCSB.js} +2 -2
  11. package/dist/chunks/{chunk-BLDUBB7X.js → chunk-4YPCUVMC.js} +2 -2
  12. package/dist/chunks/{chunk-N73RA4DR.js → chunk-6CXLLJ2C.js} +3 -3
  13. package/dist/chunks/{chunk-4SN7R4UF.js → chunk-6MD4VS2F.js} +1 -1
  14. package/dist/chunks/{chunk-VACK24HF.js → chunk-735MPLUL.js} +35 -9
  15. package/dist/chunks/chunk-7GDNDQD7.js +504 -0
  16. package/dist/chunks/{chunk-2US43KNG.js → chunk-7PEYA3NA.js} +2 -2
  17. package/dist/chunks/{chunk-XTCIKBWM.js → chunk-AXE3RR7J.js} +1 -1
  18. package/dist/chunks/{chunk-BXE3SBBN.js → chunk-C3KAPREU.js} +1 -1
  19. package/dist/chunks/{chunk-QJC63NFR.js → chunk-G3GLGDKV.js} +2 -2
  20. package/dist/chunks/{chunk-63XWYNID.js → chunk-G4WQBRDS.js} +4 -4
  21. package/dist/chunks/{chunk-SDI6BVG7.js → chunk-J4PDHWP2.js} +1 -1
  22. package/dist/chunks/{chunk-GD6RUZU5.js → chunk-JKIAHYAW.js} +14 -8
  23. package/dist/chunks/{chunk-AJQRKN3X.js → chunk-L2RQV23D.js} +1 -1
  24. package/dist/chunks/{chunk-EPOPGV5G.js → chunk-LA6JHEVV.js} +1 -1
  25. package/dist/chunks/{chunk-G7SG4XMK.js → chunk-LODKC7PJ.js} +7 -0
  26. package/dist/chunks/{chunk-QXBQDHKL.js → chunk-MO4DIRHD.js} +80 -38
  27. package/dist/chunks/{chunk-IHM43Y7B.js → chunk-N2GAA7RB.js} +1 -1
  28. package/dist/chunks/{chunk-6RB5ZQ34.js → chunk-N4AJV4GH.js} +25 -14
  29. package/dist/chunks/{chunk-LH436HS2.js → chunk-NHOXLUIL.js} +2 -2
  30. package/dist/chunks/{chunk-QLLKALSH.js → chunk-NQWEYJDD.js} +1 -1
  31. package/dist/chunks/{chunk-AWJJSA7D.js → chunk-OIOAK7BS.js} +1 -1
  32. package/dist/chunks/{chunk-UTDMFVC7.js → chunk-R3NPBUKT.js} +8 -8
  33. package/dist/chunks/{chunk-4KOYS5WR.js → chunk-R4XCTRIU.js} +1 -1
  34. package/dist/chunks/{chunk-XKPXOIWD.js → chunk-S3ICPPXC.js} +3 -3
  35. package/dist/chunks/{chunk-K4KVAMEF.js → chunk-S4CH434Z.js} +1 -1
  36. package/dist/chunks/{chunk-HMEAGAGC.js → chunk-S7BCZVRK.js} +2 -2
  37. package/dist/chunks/{chunk-SG7VQB4F.js → chunk-SDDMV2MM.js} +1 -1
  38. package/dist/chunks/{chunk-KMLATBNT.js → chunk-SIXFXXRI.js} +3 -3
  39. package/dist/chunks/{chunk-T3MEG5D3.js → chunk-U6JPSXJ3.js} +90 -21
  40. package/dist/chunks/{chunk-SENPGW4O.js → chunk-VCUOYVLC.js} +1 -1
  41. package/dist/chunks/{chunk-33KUO34R.js → chunk-VSRRIFRF.js} +1 -1
  42. package/dist/chunks/{chunk-BFUS3ENH.js → chunk-X5ULFAMM.js} +2 -2
  43. package/dist/chunks/{chunk-W5YRBEWQ.js → chunk-XOIYBEU7.js} +1 -1
  44. package/dist/chunks/{chunk-CL6ELLVF.js → chunk-YEDZ37PH.js} +1 -1
  45. package/dist/chunks/{chunk-Z4HXW6ZH.js → chunk-YF6T33PU.js} +10 -2
  46. package/dist/chunks/{chunk-2VMKQKVO.js → chunk-YIWN32ZX.js} +1 -1
  47. package/dist/chunks/{chunk-2T3V6Z3M.js → chunk-ZKIH2USC.js} +1 -1
  48. package/dist/cli/figure.js +31 -30
  49. package/dist/cli/fold.js +6 -6
  50. package/dist/deletionEdges.js +2 -2
  51. package/dist/figure.js +27 -26
  52. package/dist/figureData.js +25 -24
  53. package/dist/figureScene.js +18 -17
  54. package/dist/figureSettings.js +6 -6
  55. package/dist/foldVariants.js +2 -2
  56. package/dist/genes/genePins.js +3 -3
  57. package/dist/graphLabels.js +4 -4
  58. package/dist/index.js +45 -44
  59. package/dist/labelLayout.js +8 -8
  60. package/dist/layout/forceCache.js +6 -6
  61. package/dist/layout/mergeRuns.js +2 -2
  62. package/dist/layout/orientToReference.js +2 -2
  63. package/dist/layout/tubeMapLayout.js +9 -9
  64. package/dist/layout/walkRowDraw.js +9 -8
  65. package/dist/layout/walkRowLayout.js +3 -3
  66. package/dist/layout/walkRowRuns.js +1 -1
  67. package/dist/layout/walkRows.js +2 -2
  68. package/dist/layout/walkStrip.js +13 -12
  69. package/dist/layoutModes.js +11 -11
  70. package/dist/pathColors.js +5 -5
  71. package/dist/pipeline.js +5 -5
  72. package/dist/referenceStrip.js +9 -8
  73. package/dist/renderer/GeometryBuilder.js +8 -7
  74. package/dist/renderer/walkLanes.js +44 -0
  75. package/dist/tubeMap/axis.js +5 -5
  76. package/dist/tubeMap/bundle.js +10 -10
  77. package/dist/tubeMap/coarsen.js +10 -10
  78. package/dist/tubeMap/deviations.js +11 -11
  79. package/dist/tubeMap/draw.js +5 -5
  80. package/dist/tubeMap/genes.js +6 -6
  81. package/dist/tubeMap/nodeColors.js +9 -8
  82. package/dist/tubeMap/reads.js +2 -2
  83. package/dist/types/figureData.d.ts +22 -0
  84. package/dist/types/renderer/types.d.ts +20 -0
  85. package/dist/types/renderer/walkLanes.d.ts +60 -0
  86. package/dist/types/util/geometry.d.ts +13 -1
  87. package/dist/types/version.d.ts +1 -1
  88. package/dist/types/walkEncoding.d.ts +7 -5
  89. package/dist/types/walkHighlight.d.ts +3 -0
  90. package/dist/util/SpatialIndex.js +4 -4
  91. package/dist/util/edgeCurves.js +3 -3
  92. package/dist/util/geometry.js +7 -1
  93. package/dist/util/hitDetection.js +5 -5
  94. package/dist/version.js +1 -1
  95. package/dist/viewport.js +6 -6
  96. package/dist/walkEncoding.js +9 -8
  97. package/dist/walkHighlight.js +10 -9
  98. package/dist/walkKey.js +5 -5
  99. package/package.json +1 -1
package/README.md CHANGED
@@ -43,7 +43,7 @@ npm install @jbrowse/bandage-core
43
43
  backbone lies on. `featuresOnBackbone` renames that assembly's genes onto the
44
44
  backbone's refNames, and `genePins` pins only those
45
45
 
46
- ## Figures and the coarse tier
46
+ ## Figures
47
47
 
48
48
  - `figureSvg` draws a graph and its lifted walks to a standalone SVG. The
49
49
  `bandage-figure` CLI makes one from a JSON spec with no browser:
@@ -52,9 +52,43 @@ npm install @jbrowse/bandage-core
52
52
  - `figureData` returns what `figureSvg` would draw as columnar tables, for
53
53
  another graphics system to draw;
54
54
  [ggbandage](https://github.com/GMOD/ggbandage) draws them in ggplot2
55
- - The `bandage-fold` CLI folds every variant under a size into the reference, so
56
- a graph track can draw a whole chromosome. See
57
- [docs/coarse-tier.md](docs/coarse-tier.md)
55
+
56
+ ## Coarse tier: `bandage-fold`
57
+
58
+ `bandage-fold` writes an rGFA with every variant under `--below` bp folded into
59
+ the reference. Indexed by gfa-to-tabix, the output is the coarse tier a graph
60
+ track draws once zoomed out past the fine index, so a whole chromosome fits.
61
+ [`gfa-to-tabix fold`](https://github.com/GMOD/gfa-to-tabix/blob/main/docs/subcommands.md#fold)
62
+ documents the fold and the handover from the fine index.
63
+
64
+ ```console
65
+ npx -p @jbrowse/bandage-core bandage-fold graph.rgfa.gz --below 10000 \
66
+ | gfa-to-tabix - -o graph.fold10000
67
+ ```
68
+
69
+ - The input is a GFA or rGFA, plain or gzipped, or `-` for stdin. `-o out.gfa`
70
+ writes a file in place of stdout
71
+ - `--reference PATH` names a plain GFA's backbone path. In an rGFA, the segments
72
+ tagged `SR:i:0` form the backbone
73
+ - The fold keeps the backbone, every allele whose own length or the reference it
74
+ replaces reaches `--below`, and the shortest way from each one's ends back to
75
+ the backbone. It joins the reference between kept alleles into one segment
76
+ - `bandage-fold` drops paths and walks from the output. It writes forward a
77
+ segment that a plain GFA's path reads reversed, and flips its links to match,
78
+ so the output re-reads as the graph that was folded
79
+ - gfa-to-tabix's `--layout anchored|contig` should match the fine index's. An
80
+ anchored window inside a large bubble then returns the whole bubble in both
81
+ tiers, and a contig one only what links reach from it
82
+ - The graph track folds each cut it draws at ten pixels (`FOLD_PX` in
83
+ jbrowse-plugin-graphgenomeviewer), and folding a fold again at a larger size
84
+ closely matches folding the original at that size. A tier folded at N and
85
+ handed over at N / 10 bp per pixel, such as
86
+ `coarse: { uri: "graph.fold10000", aboveBpPerPx: 1000 }`, therefore draws
87
+ nearly what the fine cut drew
88
+ - Node caps a string at 512 MB, and `bandage-fold` reads the whole graph into
89
+ one, so it cannot fold a whole human pangenome. `gfa-to-tabix fold`, a port of
90
+ the same fold, writes the index in one pass:
91
+ `gfa-to-tabix fold graph.rgfa.gz --below 10000 -o graph.fold10000`
58
92
 
59
93
  ## Developing
60
94
 
@@ -64,21 +98,27 @@ pnpm test # unit tests
64
98
  pnpm build # dist/ with declarations
65
99
  ```
66
100
 
67
- The FMMM engine and its wasm build are in
68
- [graph-fmmm-layout](https://github.com/GMOD/graph-fmmm-layout).
69
-
70
- CI also packs the core and runs the typecheck and tests of
71
- [BandageJS](https://github.com/cmdcolin/BandageJS) and
72
- [jbrowse-plugin-graphgenomeviewer](https://github.com/GMOD/jbrowse-plugin-graphgenomeviewer)
73
- against it. For an API change, push the consumer's matching change to a
74
- `core-next` branch in that consumer's repo, which CI tests in place of `main`
75
- until the release.
76
-
77
- Release with `pnpm version patch`: it lints, tests, stamps `src/version.ts`,
78
- writes the changelog with git-cliff and pushes the `v*` tag. `publish.yml` then
79
- runs CI, consumers included, and publishes to npm with trusted publishing only
80
- if it passes. When a consumer fails, push its side to `core-next` and rerun the
81
- failed jobs.
101
+ - The FMMM engine and its wasm build are in
102
+ [graph-fmmm-layout](https://github.com/GMOD/graph-fmmm-layout)
103
+ - CI also packs the core and runs the typecheck and tests of
104
+ [BandageJS](https://github.com/cmdcolin/BandageJS) and
105
+ [jbrowse-plugin-graphgenomeviewer](https://github.com/GMOD/jbrowse-plugin-graphgenomeviewer)
106
+ against it. For an API change, push the consumer's matching change to a
107
+ `core-next` branch in that consumer's repo, which CI tests in place of `main`
108
+ until the release
109
+ - `src/foldParity.test.ts` folds a few hundred random rGFA graphs with
110
+ `foldVariants` and with the gfa-to-tabix release that
111
+ `.github/actions/gfa-to-tabix` pins, and fails when they fold differently.
112
+ Locally the test runs the `gfa-to-tabix` on PATH or named by `GFA_TO_TABIX`,
113
+ and skips without one. In the other direction, gfa-to-tabix's
114
+ `scripts/parity-fold.sh` checks that `gfa-to-tabix fold` writes the same bytes
115
+ as `bandage-fold | gfa-to-tabix -`, and its CI tests the port against a pinned
116
+ bandage-core
117
+ - Release with `pnpm version patch`: it lints, tests, stamps `src/version.ts`,
118
+ writes the changelog with git-cliff and pushes the `v*` tag. `publish.yml`
119
+ then runs CI, consumers included, and publishes to npm with trusted publishing
120
+ only if it passes. When a consumer fails, push its side to `core-next` and
121
+ rerun the failed jobs
82
122
 
83
123
  ## See also
84
124
 
@@ -1,17 +1,17 @@
1
1
  import {
2
2
  bubbleHalos,
3
3
  coversGraph
4
- } from "../chunks/chunk-BLDUBB7X.js";
5
- import "../chunks/chunk-AWJJSA7D.js";
4
+ } from "../chunks/chunk-4YPCUVMC.js";
5
+ import "../chunks/chunk-OIOAK7BS.js";
6
6
  import "../chunks/chunk-FX4RDMDN.js";
7
- import "../chunks/chunk-N73RA4DR.js";
7
+ import "../chunks/chunk-6CXLLJ2C.js";
8
8
  import "../chunks/chunk-HMXCN6PG.js";
9
9
  import "../chunks/chunk-AB35TJST.js";
10
- import "../chunks/chunk-2VMKQKVO.js";
11
- import "../chunks/chunk-P276TWPM.js";
10
+ import "../chunks/chunk-YIWN32ZX.js";
11
+ import "../chunks/chunk-3CXZUC2Y.js";
12
12
  import "../chunks/chunk-TLWXLNLY.js";
13
13
  import "../chunks/chunk-3YBP46PB.js";
14
- import "../chunks/chunk-VACK24HF.js";
14
+ import "../chunks/chunk-735MPLUL.js";
15
15
  import "../chunks/chunk-CPMHA4W5.js";
16
16
  import "../chunks/chunk-ZPZ7GNBT.js";
17
17
  import "../chunks/chunk-BSOZW3FC.js";
@@ -3,16 +3,16 @@ import {
3
3
  BUBBLE_KIND_NAMES,
4
4
  bubbleSegmentIds,
5
5
  classifyBubble
6
- } from "../chunks/chunk-AWJJSA7D.js";
6
+ } from "../chunks/chunk-OIOAK7BS.js";
7
7
  import "../chunks/chunk-FX4RDMDN.js";
8
- import "../chunks/chunk-N73RA4DR.js";
8
+ import "../chunks/chunk-6CXLLJ2C.js";
9
9
  import "../chunks/chunk-HMXCN6PG.js";
10
10
  import "../chunks/chunk-AB35TJST.js";
11
- import "../chunks/chunk-2VMKQKVO.js";
12
- import "../chunks/chunk-P276TWPM.js";
11
+ import "../chunks/chunk-YIWN32ZX.js";
12
+ import "../chunks/chunk-3CXZUC2Y.js";
13
13
  import "../chunks/chunk-TLWXLNLY.js";
14
14
  import "../chunks/chunk-3YBP46PB.js";
15
- import "../chunks/chunk-VACK24HF.js";
15
+ import "../chunks/chunk-735MPLUL.js";
16
16
  import "../chunks/chunk-CPMHA4W5.js";
17
17
  import "../chunks/chunk-ZPZ7GNBT.js";
18
18
  import "../chunks/chunk-BSOZW3FC.js";
@@ -1,22 +1,22 @@
1
1
  import {
2
2
  graphBubbles,
3
3
  superbubblesFromGraph
4
- } from "../chunks/chunk-KMLATBNT.js";
5
- import "../chunks/chunk-BLDUBB7X.js";
4
+ } from "../chunks/chunk-SIXFXXRI.js";
5
+ import "../chunks/chunk-4YPCUVMC.js";
6
6
  import "../chunks/chunk-RFG42VIO.js";
7
- import "../chunks/chunk-AWJJSA7D.js";
7
+ import "../chunks/chunk-OIOAK7BS.js";
8
8
  import "../chunks/chunk-FX4RDMDN.js";
9
9
  import "../chunks/chunk-MHZTTLBU.js";
10
10
  import "../chunks/chunk-UAFATXYA.js";
11
11
  import "../chunks/chunk-VX656OAZ.js";
12
- import "../chunks/chunk-N73RA4DR.js";
12
+ import "../chunks/chunk-6CXLLJ2C.js";
13
13
  import "../chunks/chunk-HMXCN6PG.js";
14
14
  import "../chunks/chunk-AB35TJST.js";
15
- import "../chunks/chunk-2VMKQKVO.js";
16
- import "../chunks/chunk-P276TWPM.js";
15
+ import "../chunks/chunk-YIWN32ZX.js";
16
+ import "../chunks/chunk-3CXZUC2Y.js";
17
17
  import "../chunks/chunk-TLWXLNLY.js";
18
18
  import "../chunks/chunk-3YBP46PB.js";
19
- import "../chunks/chunk-VACK24HF.js";
19
+ import "../chunks/chunk-735MPLUL.js";
20
20
  import "../chunks/chunk-CPMHA4W5.js";
21
21
  import "../chunks/chunk-ZPZ7GNBT.js";
22
22
  import "../chunks/chunk-BSOZW3FC.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ownStrand
3
- } from "./chunk-VACK24HF.js";
3
+ } from "./chunk-735MPLUL.js";
4
4
  import {
5
5
  isBackbone
6
6
  } from "./chunk-RJWPJCIB.js";
@@ -1,3 +1,19 @@
1
+ import {
2
+ LANE_SMOOTHING_PX,
3
+ THREAD_PITCH_PX,
4
+ THREAD_PX,
5
+ THREAD_REACH_PX,
6
+ THREAD_TAPER_PX,
7
+ bandScale,
8
+ bandSpans,
9
+ bandWidthPx,
10
+ laneLines,
11
+ laneRuns,
12
+ laneSlideAt,
13
+ slotCentres,
14
+ threadsPasses,
15
+ walkThreads
16
+ } from "./chunk-7GDNDQD7.js";
1
17
  import {
2
18
  abgrToCssRgba,
3
19
  fadeAbgr,
@@ -10,13 +26,13 @@ import {
10
26
  } from "./chunk-GQZVR5DW.js";
11
27
  import {
12
28
  baseEdgeCurves
13
- } from "./chunk-33KUO34R.js";
29
+ } from "./chunk-VSRRIFRF.js";
14
30
  import {
15
31
  PATH_LIGHTNESS,
16
32
  PATH_SATURATION,
17
33
  nameHue,
18
34
  pathHueAt
19
- } from "./chunk-GD6RUZU5.js";
35
+ } from "./chunk-JKIAHYAW.js";
20
36
  import {
21
37
  meanDepth,
22
38
  nodeWidthPx,
@@ -26,13 +42,14 @@ import {
26
42
  referenceMidpoints
27
43
  } from "./chunk-VX656OAZ.js";
28
44
  import {
29
- dashCurves,
45
+ dashSpans,
30
46
  pathRibbonOffsets,
31
47
  polylinePx,
48
+ slideCurves,
32
49
  strandSides,
33
50
  translateCurves,
34
51
  yToXOf
35
- } from "./chunk-VACK24HF.js";
52
+ } from "./chunk-735MPLUL.js";
36
53
  import {
37
54
  pushTo
38
55
  } from "./chunk-BSOZW3FC.js";
@@ -149,13 +166,16 @@ function arrowheadFor({
149
166
  color
150
167
  };
151
168
  }
152
- function leavesStart(edge, segments, first, stranded, yToX) {
153
- if (stranded) {
154
- return strandSides(edge).from === "start";
155
- }
156
- const gap = (p) => Math.hypot(p.x - first.x0, (p.y - first.y0) * yToX);
169
+ var FIRST_THREADS = [[0, 0]];
170
+ var ONE_PASS = [0];
171
+ var NO_SHIFT = { dx: 0, dy: 0 };
172
+ function nearerStart(at, segments, yToX) {
173
+ const gap = (p) => Math.hypot(p.x - at.x, (p.y - at.y) * yToX);
157
174
  return gap(segments[0]) < gap(segments.at(-1));
158
175
  }
176
+ function leavesStart(edge, segments, first, stranded, yToX) {
177
+ return stranded ? strandSides(edge).from === "start" : nearerStart({ x: first.x0, y: first.y0 }, segments, yToX);
178
+ }
159
179
  function intoDirection(last, toSegments) {
160
180
  if (toSegments.length < 2) {
161
181
  return void 0;
@@ -496,6 +516,74 @@ function buildGeometry(options) {
496
516
  const { pathColorByIndex, pathIndexByName } = pathPaletteOf(graph);
497
517
  const pathColorByName = (name) => pathColorByIndex[pathIndexByName.get(name) ?? -1] ?? EDGE_PATH_FALLBACK_COLOR;
498
518
  const nodePathSlots = drawPaths && pathCount > 1 ? pathSlotsOf(graph) : NO_SLOTS;
519
+ const liftedWalks = highlight?.walks ?? [];
520
+ const threaded = threadsPasses(liftedWalks);
521
+ const lanePasses = liftedWalks.map((w) => w.mostPasses);
522
+ const thinned = threaded ? bandScale(lanePasses) : 1;
523
+ const runs = liftedWalks.length > 1 ? laneRuns(
524
+ liftedWalks.flatMap((w) => w.route),
525
+ nodePositions,
526
+ yToX,
527
+ LANE_SMOOTHING_PX * worldPerScreenPx
528
+ ) : void 0;
529
+ const lines = runs ? laneLines(
530
+ runs,
531
+ nodePositions,
532
+ yToX,
533
+ LANE_SMOOTHING_PX * worldPerScreenPx,
534
+ bandWidthPx(lanePasses) * thinned * worldPerScreenPx,
535
+ threaded
536
+ ) : void 0;
537
+ const threads = threaded ? liftedWalks.map(
538
+ (w) => walkThreads(
539
+ w.route,
540
+ w.passes,
541
+ nodePositions,
542
+ yToX,
543
+ THREAD_REACH_PX * worldPerScreenPx
544
+ )
545
+ ) : void 0;
546
+ const spanField = runs && threads ? bandSpans(
547
+ runs,
548
+ threads.map((t) => t.span),
549
+ {
550
+ reach: THREAD_REACH_PX * worldPerScreenPx,
551
+ taper: THREAD_TAPER_PX * worldPerScreenPx
552
+ }
553
+ ) : void 0;
554
+ const edgeKeys = new Set(
555
+ threads ? graph.edges.map((e) => `${e.from}>${e.to}`) : []
556
+ );
557
+ const centresByNode = /* @__PURE__ */ new Map();
558
+ const laneOffset = (nodeId, laneWidth, lane, pass) => {
559
+ if (!threaded) {
560
+ return (lane - (liftedWalks.length - 1) / 2) * laneWidth;
561
+ }
562
+ let centres = centresByNode.get(nodeId);
563
+ if (!centres) {
564
+ const spans = spanField?.get(nodeId);
565
+ centres = slotCentres(spans?.slot ?? lanePasses, lanePasses, spans?.held);
566
+ centresByNode.set(nodeId, centres);
567
+ }
568
+ return (centres[lane] + pass * THREAD_PITCH_PX) * thinned;
569
+ };
570
+ const laneHalfWidth = (laneWidth) => threaded ? THREAD_PX / 2 * thinned : laneWidth / 2;
571
+ const laneNormals = /* @__PURE__ */ new Map();
572
+ const laneNormalsOf = (nodeId, segments) => {
573
+ let normals = laneNormals.get(nodeId);
574
+ if (!normals) {
575
+ const smoothed = lines?.normals.get(nodeId);
576
+ normals = smoothed ? pointNormalsOf(segments, yToX).map((n, i) => smoothed[i] ?? n) : pointNormalsOf(segments, yToX);
577
+ laneNormals.set(nodeId, normals);
578
+ }
579
+ return normals;
580
+ };
581
+ const laneWidths = /* @__PURE__ */ new Map();
582
+ const laneShiftOf = (nodeId, index) => lines?.shifts.get(nodeId)?.[index] ?? NO_SHIFT;
583
+ const lanePointsOf = (nodeId, segments) => lines?.shifts.has(nodeId) ? segments.map((p, i) => {
584
+ const { dx, dy } = laneShiftOf(nodeId, i);
585
+ return { ...p, x: p.x + dx, y: p.y + dy };
586
+ }) : segments;
499
587
  const showArrows = scale > (linearLayout ? 1 : MIN_ARROW_SCALE);
500
588
  for (let ei = 0; ei < graph.edges.length; ei++) {
501
589
  const edge = graph.edges[ei];
@@ -516,18 +604,37 @@ function buildGeometry(options) {
516
604
  const toNode = nodeById.get(edge.to);
517
605
  const intoHalfWidth = (toNode ? nodeWidthPx(toNode, contigThickness, nodeWidth, depthNorm) : contigThickness) / 2;
518
606
  const inView = (curves) => !viewportBounds || isBezierInBounds(curves, viewportBounds);
519
- const stroke = (curves, thickness, color) => {
520
- if (isDeletion) {
521
- for (const dash of dashCurves(
522
- curves,
523
- DELETION_DASH_PX / scale,
524
- yToX,
525
- viewportBounds
526
- )) {
527
- edgeCurves.push({ curves: dash, thickness, color });
607
+ const stroke = (curves, thickness, color, lane) => {
608
+ const push = (pieces, from = 0, to = 1) => {
609
+ if (!lane) {
610
+ edgeCurves.push({ curves: pieces, thickness, color });
611
+ return;
528
612
  }
529
- } else {
530
- edgeCurves.push({ curves, thickness, color });
613
+ const start = from === 0 ? lane.start : laneSlideAt(lane.start, lane.end, from);
614
+ const end = to === 1 ? lane.end : laneSlideAt(lane.start, lane.end, to);
615
+ edgeCurves.push({
616
+ curves: pieces,
617
+ thickness,
618
+ color,
619
+ walk: lane.walk,
620
+ pass: lane.pass,
621
+ normal: start.normal,
622
+ offsetX: start.offsetX,
623
+ endNormal: end.normal,
624
+ endOffsetX: end.offsetX
625
+ });
626
+ };
627
+ if (!isDeletion) {
628
+ push(curves);
629
+ return;
630
+ }
631
+ for (const dash of dashSpans(
632
+ curves,
633
+ DELETION_DASH_PX / scale,
634
+ yToX,
635
+ viewportBounds
636
+ )) {
637
+ push(dash.curves, dash.startAlong, dash.endAlong);
531
638
  }
532
639
  };
533
640
  const arrowOn = (curves) => {
@@ -549,32 +656,86 @@ function buildGeometry(options) {
549
656
  const arrowStart = arrows.length;
550
657
  const laneWalks = highlight?.walks.length && onWalk && !ribbons ? highlight.walks : void 0;
551
658
  if (laneWalks) {
552
- const fromNode = nodeById.get(edge.from);
553
- const width = fromNode ? nodeWidthPx(fromNode, contigThickness, nodeWidth, depthNorm) : contigThickness;
554
- const laneWidth = walkLaneWidthPx(width, laneWalks.length);
555
- const normals = pointNormalsOf(fromSegments, yToX);
556
- const { nx, ny } = (leavesStart(
557
- edge,
659
+ const laneWidthOf = (id) => {
660
+ let width = laneWidths.get(id);
661
+ if (width === void 0) {
662
+ const node = nodeById.get(id);
663
+ width = walkLaneWidthPx(
664
+ node ? nodeWidthPx(node, contigThickness, nodeWidth, depthNorm) : contigThickness,
665
+ laneWalks.length
666
+ );
667
+ laneWidths.set(id, width);
668
+ }
669
+ return width;
670
+ };
671
+ const endOf = (id, segments, atStart) => {
672
+ const at = atStart ? 0 : segments.length - 1;
673
+ return {
674
+ id,
675
+ laneWidth: laneWidthOf(id),
676
+ normal: laneNormalsOf(id, segments)[at] ?? { nx: 0, ny: 0 },
677
+ shift: laneShiftOf(id, at)
678
+ };
679
+ };
680
+ const from = endOf(
681
+ edge.from,
558
682
  fromSegments,
559
- baseCurves[0],
560
- stranded,
561
- yToX
562
- ) ? normals[0] : normals.at(-1)) ?? {
563
- nx: 0,
564
- ny: 0
683
+ leavesStart(edge, fromSegments, baseCurves[0], stranded, yToX)
684
+ );
685
+ const last = baseCurves.at(-1);
686
+ const to = threaded ? endOf(
687
+ edge.to,
688
+ toSegments,
689
+ nearerStart({ x: last.x1, y: last.y1 }, toSegments, yToX)
690
+ ) : from;
691
+ const slideOf = (end, offsetX) => ({
692
+ dx: end.shift.dx + end.normal.nx * offsetX,
693
+ dy: end.shift.dy + end.normal.ny * offsetX / yToX
694
+ });
695
+ const aheadKey = threads ? `${edge.from}>${edge.to}` : "";
696
+ const backKey = threads ? `${edge.to}>${edge.from}` : "";
697
+ const pairsOf = (lane) => {
698
+ const own = threads?.[lane]?.linkThreads;
699
+ if (!own?.size) {
700
+ return FIRST_THREADS;
701
+ }
702
+ const ahead = own.get(aheadKey);
703
+ const back = edgeKeys.has(backKey) ? void 0 : own.get(backKey);
704
+ if (!back) {
705
+ return ahead ?? FIRST_THREADS;
706
+ }
707
+ const turned = back.map(([a, b]) => [b, a]);
708
+ return ahead ? [...ahead, ...turned] : turned;
565
709
  };
710
+ const single = laneWalks.length === 1;
566
711
  laneWalks.forEach((walk, lane) => {
567
712
  if (!walk.edgeIndexes.has(ei)) {
568
713
  return;
569
714
  }
570
- const offset = (lane - (laneWalks.length - 1) / 2) * laneWidth * worldPerScreenPx;
571
- const curves = offset === 0 ? baseCurves : translateCurves(baseCurves, nx * offset, ny * offset / yToX);
572
- if (inView(curves)) {
573
- stroke(
574
- curves,
575
- laneWidth / 2,
576
- walk.colors.get(edge.from) ?? walk.colors.get(edge.to) ?? plainEdgeColor
715
+ for (const [pass, toPass] of pairsOf(lane)) {
716
+ const offsetX = single ? 0 : laneOffset(from.id, from.laneWidth, lane, pass) * worldPerScreenPx;
717
+ const start = { normal: from.normal, offsetX };
718
+ const end = threaded ? {
719
+ normal: to.normal,
720
+ offsetX: laneOffset(to.id, to.laneWidth, lane, toPass) * worldPerScreenPx
721
+ } : start;
722
+ const curves = threaded ? slideCurves(
723
+ baseCurves,
724
+ slideOf(from, start.offsetX),
725
+ slideOf(to, end.offsetX)
726
+ ) : offsetX === 0 ? baseCurves : translateCurves(
727
+ baseCurves,
728
+ from.normal.nx * offsetX,
729
+ from.normal.ny * offsetX / yToX
577
730
  );
731
+ if (inView(curves)) {
732
+ stroke(
733
+ curves,
734
+ laneHalfWidth(from.laneWidth),
735
+ walk.colors.get(edge.from) ?? walk.colors.get(edge.to) ?? plainEdgeColor,
736
+ { walk: lane, pass, start, end }
737
+ );
738
+ }
578
739
  }
579
740
  });
580
741
  if (edgeCurves.length > edgeCurveStart) {
@@ -644,17 +805,34 @@ function buildGeometry(options) {
644
805
  const lanes = highlight?.walks.length && !faded && !slots?.length ? highlight.walks : void 0;
645
806
  if (drawable && lanes) {
646
807
  const laneWidth = walkLaneWidthPx(width, lanes.length);
647
- const normals = pointNormalsOf(segments, yToX);
808
+ const normals = lanes.length > 1 ? laneNormalsOf(nodeId, segments) : [];
809
+ const points = lanes.length > 1 ? lanePointsOf(nodeId, segments) : segments;
648
810
  lanes.forEach((walk, lane) => {
649
811
  const laneColor = walk.colors.get(nodeId);
650
812
  if (laneColor === void 0) {
651
813
  return;
652
814
  }
653
- const offset = (lane - (lanes.length - 1) / 2) * laneWidth * worldPerScreenPx;
654
- nodeStrokes.push({
655
- points: lanes.length === 1 ? segments : offsetPolyline(segments, normals, offset, yToX),
656
- thickness: laneWidth / 2,
657
- color: laneColor
815
+ const onThreads = threads?.[lane]?.nodeThreads.get(nodeId) ?? ONE_PASS;
816
+ const thickness = laneHalfWidth(laneWidth);
817
+ onThreads.forEach((pass) => {
818
+ const offsetX = lanes.length === 1 ? 0 : laneOffset(nodeId, laneWidth, lane, pass) * worldPerScreenPx;
819
+ nodeStrokes.push(
820
+ lanes.length === 1 ? {
821
+ points: segments,
822
+ thickness,
823
+ color: laneColor,
824
+ walk: lane,
825
+ pass
826
+ } : {
827
+ points: offsetPolyline(points, normals, offsetX, yToX),
828
+ thickness,
829
+ color: laneColor,
830
+ walk: lane,
831
+ pass,
832
+ normals,
833
+ offsetX
834
+ }
835
+ );
658
836
  });
659
837
  });
660
838
  } else if (drawable && slots?.length && slotWidth >= MIN_PATH_STRIPE_PX) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  clampZoom
3
- } from "./chunk-XKPXOIWD.js";
3
+ } from "./chunk-S3ICPPXC.js";
4
4
 
5
5
  // src/viewport.ts
6
6
  function axisScaleOf(scale, pixelRows) {
@@ -9,7 +9,7 @@ import {
9
9
  edgeJoinOf,
10
10
  routedEdgeCurves,
11
11
  strandSides
12
- } from "./chunk-VACK24HF.js";
12
+ } from "./chunk-735MPLUL.js";
13
13
  import {
14
14
  groupBy
15
15
  } from "./chunk-BSOZW3FC.js";
@@ -3,7 +3,7 @@ import {
3
3
  hasTubeMapPaths,
4
4
  tubeMapLayout,
5
5
  tubeMapReferenceLayout
6
- } from "./chunk-63XWYNID.js";
6
+ } from "./chunk-G4WQBRDS.js";
7
7
  import {
8
8
  sampleRowLayout
9
9
  } from "./chunk-XR2HMGAS.js";
@@ -15,7 +15,7 @@ import {
15
15
  } from "./chunk-UAFATXYA.js";
16
16
  import {
17
17
  walkRowLayout
18
- } from "./chunk-SENPGW4O.js";
18
+ } from "./chunk-VCUOYVLC.js";
19
19
  import {
20
20
  isBackbone,
21
21
  isOffReference
@@ -3,14 +3,14 @@ import {
3
3
  boxesGenes,
4
4
  rowPitch,
5
5
  rowSpan
6
- } from "./chunk-EPOPGV5G.js";
6
+ } from "./chunk-LA6JHEVV.js";
7
7
  import {
8
8
  el
9
9
  } from "./chunk-H2H4Y3TV.js";
10
10
  import {
11
11
  READOUT_ROOM,
12
12
  longestBar
13
- } from "./chunk-SENPGW4O.js";
13
+ } from "./chunk-VCUOYVLC.js";
14
14
  import {
15
15
  LABEL_CHAR_PX
16
16
  } from "./chunk-AB35TJST.js";
@@ -1,10 +1,10 @@
1
1
  import {
2
2
  bubbleSegmentIds,
3
3
  classifyBubble
4
- } from "./chunk-AWJJSA7D.js";
4
+ } from "./chunk-OIOAK7BS.js";
5
5
  import {
6
6
  svgPath
7
- } from "./chunk-VACK24HF.js";
7
+ } from "./chunk-735MPLUL.js";
8
8
  import {
9
9
  isBackbone
10
10
  } from "./chunk-RJWPJCIB.js";
@@ -7,15 +7,15 @@ import {
7
7
  } from "./chunk-AB35TJST.js";
8
8
  import {
9
9
  polylineMidpoint
10
- } from "./chunk-2VMKQKVO.js";
10
+ } from "./chunk-YIWN32ZX.js";
11
11
  import {
12
12
  edgeCurvesOf
13
- } from "./chunk-P276TWPM.js";
13
+ } from "./chunk-3CXZUC2Y.js";
14
14
  import {
15
15
  curveBounds,
16
16
  curveMidpoint,
17
17
  polylinePx
18
- } from "./chunk-VACK24HF.js";
18
+ } from "./chunk-735MPLUL.js";
19
19
 
20
20
  // src/graphLabels.ts
21
21
  var MIN_DELETION_LABEL_PX = 26;
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  ENGINE_DEFAULTS
3
- } from "./chunk-XKPXOIWD.js";
3
+ } from "./chunk-S3ICPPXC.js";
4
4
 
5
5
  // src/figureSettings.ts
6
6
  var FIGURE_DEFAULTS = {