@jbrowse/bandage-core 5.1.0 → 5.2.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.
@@ -1804,7 +1804,7 @@ function placeLabels(candidates, frame, take) {
1804
1804
  if (offFrame(box, frame)) {
1805
1805
  return void 0;
1806
1806
  }
1807
- if (insideFrame(box, frame) && take(box)) {
1807
+ if (insideFrame(box, frame) && (c.fits?.(box) ?? true) && take(box)) {
1808
1808
  return { ...c, text, y, w };
1809
1809
  }
1810
1810
  }
@@ -4432,6 +4432,415 @@ function groupByPaint(items, paintOf) {
4432
4432
  return [...groups.values(), ...deferred.values()];
4433
4433
  }
4434
4434
 
4435
+ // src/util/SpatialIndex.ts
4436
+ var FALLBACK_CELL_SIZE = 50;
4437
+ function meanExtentCellSize(extentTotal, count) {
4438
+ return count > 0 ? Math.max(1, extentTotal / count) : FALLBACK_CELL_SIZE;
4439
+ }
4440
+ function rowPitchOf(boxes) {
4441
+ const distinct = /* @__PURE__ */ new Set();
4442
+ let min = Infinity;
4443
+ let max = -Infinity;
4444
+ for (const box of boxes) {
4445
+ distinct.add(box.minY);
4446
+ min = Math.min(min, box.minY);
4447
+ max = Math.max(max, box.minY);
4448
+ }
4449
+ return distinct.size > 1 ? (max - min) / (distinct.size - 1) : void 0;
4450
+ }
4451
+ function cellSizeFrom(boxes, extentX, extentY) {
4452
+ const x = meanExtentCellSize(extentX, boxes.length);
4453
+ return {
4454
+ x,
4455
+ y: extentY > 0 ? meanExtentCellSize(extentY, boxes.length) : rowPitchOf(boxes) ?? x
4456
+ };
4457
+ }
4458
+ function resolveCellSize(given, boxes, extentX, extentY) {
4459
+ return given === void 0 ? cellSizeFrom(boxes, extentX, extentY) : typeof given === "number" ? { x: given, y: given } : given;
4460
+ }
4461
+ function getOrCreateCell(cells, cx, cy) {
4462
+ let row = cells.get(cx);
4463
+ if (!row) {
4464
+ row = /* @__PURE__ */ new Map();
4465
+ cells.set(cx, row);
4466
+ }
4467
+ let cell = row.get(cy);
4468
+ if (!cell) {
4469
+ cell = [];
4470
+ row.set(cy, cell);
4471
+ }
4472
+ return cell;
4473
+ }
4474
+ function addToGrid(cells, cellSize, minX, minY, maxX, maxY, entry) {
4475
+ const cx0 = Math.floor(minX / cellSize.x);
4476
+ const cx1 = Math.floor(maxX / cellSize.x);
4477
+ const cy0 = Math.floor(minY / cellSize.y);
4478
+ const cy1 = Math.floor(maxY / cellSize.y);
4479
+ for (let cx = cx0; cx <= cx1; cx++) {
4480
+ for (let cy = cy0; cy <= cy1; cy++) {
4481
+ getOrCreateCell(cells, cx, cy).push(entry);
4482
+ }
4483
+ }
4484
+ }
4485
+ function queryGrid(cells, cellSize, x, y, radiusX, radiusY) {
4486
+ const cx0 = Math.floor((x - radiusX) / cellSize.x);
4487
+ const cx1 = Math.floor((x + radiusX) / cellSize.x);
4488
+ const cy0 = Math.floor((y - radiusY) / cellSize.y);
4489
+ const cy1 = Math.floor((y + radiusY) / cellSize.y);
4490
+ const results = [];
4491
+ const seen = /* @__PURE__ */ new Set();
4492
+ for (let cx = cx0; cx <= cx1; cx++) {
4493
+ const row = cells.get(cx);
4494
+ if (row) {
4495
+ for (let cy = cy0; cy <= cy1; cy++) {
4496
+ const cell = row.get(cy);
4497
+ if (cell) {
4498
+ for (const entry of cell) {
4499
+ if (!seen.has(entry)) {
4500
+ seen.add(entry);
4501
+ results.push(entry);
4502
+ }
4503
+ }
4504
+ }
4505
+ }
4506
+ }
4507
+ }
4508
+ return results;
4509
+ }
4510
+ var SpatialIndex = class {
4511
+ constructor(nodePositions, cellSize) {
4512
+ this.cells = /* @__PURE__ */ new Map();
4513
+ const boxes = [];
4514
+ let extentX = 0;
4515
+ let extentY = 0;
4516
+ for (const [nodeId, segments] of Object.entries(nodePositions)) {
4517
+ for (let i = 0; i < segments.length - 1; i++) {
4518
+ const seg = segments[i];
4519
+ const next = segments[i + 1];
4520
+ const minX = Math.min(seg.x, next.x);
4521
+ const minY = Math.min(seg.y, next.y);
4522
+ const maxX = Math.max(seg.x, next.x);
4523
+ const maxY = Math.max(seg.y, next.y);
4524
+ extentX += maxX - minX;
4525
+ extentY += maxY - minY;
4526
+ boxes.push({ entry: { nodeId, segmentIdx: i }, minX, minY, maxX, maxY });
4527
+ }
4528
+ }
4529
+ this.cellSize = resolveCellSize(cellSize, boxes, extentX, extentY);
4530
+ for (const b of boxes) {
4531
+ addToGrid(
4532
+ this.cells,
4533
+ this.cellSize,
4534
+ b.minX,
4535
+ b.minY,
4536
+ b.maxX,
4537
+ b.maxY,
4538
+ b.entry
4539
+ );
4540
+ }
4541
+ }
4542
+ query(x, y, radiusX, radiusY = radiusX) {
4543
+ return queryGrid(this.cells, this.cellSize, x, y, radiusX, radiusY);
4544
+ }
4545
+ };
4546
+ var EdgeSpatialIndex = class {
4547
+ constructor(nodePositions, graph, drawPaths, axis, cellSize, deletions, version2 = 0, routes, stranded = false) {
4548
+ this.cells = /* @__PURE__ */ new Map();
4549
+ this.edgeCurves = baseEdgeCurves(
4550
+ nodePositions,
4551
+ graph,
4552
+ axis,
4553
+ deletions,
4554
+ version2,
4555
+ routes,
4556
+ stranded
4557
+ );
4558
+ const boxes = [];
4559
+ let extentX = 0;
4560
+ let extentY = 0;
4561
+ for (let ei = 0; ei < graph.edges.length; ei++) {
4562
+ const edge = graph.edges[ei];
4563
+ const fromSegments = nodePositions[edge.from];
4564
+ const toSegments = nodePositions[edge.to];
4565
+ const curves = this.edgeCurves.get(ei);
4566
+ if (fromSegments?.length && toSegments?.length && curves) {
4567
+ let { minX, minY, maxX, maxY } = curveBounds(curves);
4568
+ const numPaths = edge.pathIds?.length ?? 0;
4569
+ if (drawPaths && numPaths > 0) {
4570
+ let padX = 0;
4571
+ let padY = 0;
4572
+ for (const o of pathRibbonOffsets(
4573
+ fromSegments,
4574
+ toSegments,
4575
+ numPaths,
4576
+ axis
4577
+ )) {
4578
+ padX = Math.max(padX, Math.abs(o.x));
4579
+ padY = Math.max(padY, Math.abs(o.y));
4580
+ }
4581
+ minX -= padX;
4582
+ minY -= padY;
4583
+ maxX += padX;
4584
+ maxY += padY;
4585
+ }
4586
+ extentX += maxX - minX;
4587
+ extentY += maxY - minY;
4588
+ boxes.push({ ei, minX, minY, maxX, maxY });
4589
+ }
4590
+ }
4591
+ this.cellSize = resolveCellSize(cellSize, boxes, extentX, extentY);
4592
+ for (const b of boxes) {
4593
+ addToGrid(this.cells, this.cellSize, b.minX, b.minY, b.maxX, b.maxY, b.ei);
4594
+ }
4595
+ }
4596
+ getCurves(ei) {
4597
+ return this.edgeCurves.get(ei);
4598
+ }
4599
+ query(x, y, radiusX, radiusY = radiusX) {
4600
+ return queryGrid(this.cells, this.cellSize, x, y, radiusX, radiusY);
4601
+ }
4602
+ };
4603
+
4604
+ // src/util/hitDetection.ts
4605
+ function distanceToSegment(px, py, x1, y1, x2, y2) {
4606
+ const dx = x2 - x1;
4607
+ const dy = y2 - y1;
4608
+ const lenSq = dx * dx + dy * dy;
4609
+ if (lenSq === 0) {
4610
+ return Math.hypot(px - x1, py - y1);
4611
+ }
4612
+ const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lenSq));
4613
+ return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy));
4614
+ }
4615
+ function distanceToCubicBezier(px, py, x1, y1, cx1, cy1, cx2, cy2, x2, y2) {
4616
+ let minDist = Infinity;
4617
+ const samples = 20;
4618
+ let lastX = x1;
4619
+ let lastY = y1;
4620
+ for (let i = 1; i <= samples; i++) {
4621
+ const t = i / samples;
4622
+ const u = 1 - t;
4623
+ const bx = u * u * u * x1 + 3 * u * u * t * cx1 + 3 * u * t * t * cx2 + t * t * t * x2;
4624
+ const by = u * u * u * y1 + 3 * u * u * t * cy1 + 3 * u * t * t * cy2 + t * t * t * y2;
4625
+ minDist = Math.min(minDist, distanceToSegment(px, py, lastX, lastY, bx, by));
4626
+ lastX = bx;
4627
+ lastY = by;
4628
+ }
4629
+ return minDist;
4630
+ }
4631
+ function distanceToBezierCurve(px, py, c) {
4632
+ return distanceToCubicBezier(
4633
+ px,
4634
+ py,
4635
+ c.x0,
4636
+ c.y0,
4637
+ c.cx0,
4638
+ c.cy0,
4639
+ c.cx1,
4640
+ c.cy1,
4641
+ c.x1,
4642
+ c.y1
4643
+ );
4644
+ }
4645
+ function distanceToEdgeCurves(px, py, curves, yToX = 1) {
4646
+ let minDist = Infinity;
4647
+ for (const c of curves) {
4648
+ const d = distanceToBezierCurve(
4649
+ px,
4650
+ py,
4651
+ yToX === 1 ? c : {
4652
+ x0: c.x0,
4653
+ y0: c.y0 * yToX,
4654
+ cx0: c.cx0,
4655
+ cy0: c.cy0 * yToX,
4656
+ cx1: c.cx1,
4657
+ cy1: c.cy1 * yToX,
4658
+ x1: c.x1,
4659
+ y1: c.y1 * yToX
4660
+ }
4661
+ );
4662
+ if (d < minDist) {
4663
+ minDist = d;
4664
+ }
4665
+ }
4666
+ return minDist;
4667
+ }
4668
+ var nodeCache = /* @__PURE__ */ new WeakMap();
4669
+ function getSpatialIndex(nodePositions, version2) {
4670
+ const cached2 = nodeCache.get(nodePositions);
4671
+ if (cached2?.version === version2) {
4672
+ return cached2.index;
4673
+ }
4674
+ const index = new SpatialIndex(nodePositions);
4675
+ nodeCache.set(nodePositions, { version: version2, index });
4676
+ return index;
4677
+ }
4678
+ var edgeCache = /* @__PURE__ */ new WeakMap();
4679
+ function getEdgeSpatialIndex(nodePositions, graph, drawPaths, axis, version2, deletions, routes, stranded = false) {
4680
+ const cached2 = edgeCache.get(nodePositions);
4681
+ if (cached2?.graph === graph && cached2.drawPaths === drawPaths && cached2.scaleX === axis.scaleX && cached2.scaleY === axis.scaleY && cached2.version === version2 && cached2.deletions === deletions && cached2.routes === routes && cached2.stranded === stranded) {
4682
+ return cached2.index;
4683
+ }
4684
+ const index = new EdgeSpatialIndex(
4685
+ nodePositions,
4686
+ graph,
4687
+ drawPaths,
4688
+ axis,
4689
+ void 0,
4690
+ deletions,
4691
+ version2,
4692
+ routes,
4693
+ stranded
4694
+ );
4695
+ edgeCache.set(nodePositions, {
4696
+ graph,
4697
+ drawPaths,
4698
+ scaleX: axis.scaleX,
4699
+ scaleY: axis.scaleY,
4700
+ version: version2,
4701
+ index,
4702
+ deletions,
4703
+ routes,
4704
+ stranded
4705
+ });
4706
+ return index;
4707
+ }
4708
+ var MIN_NODE_REACH_PX = 5;
4709
+ function findHoveredNode(nodePositions, graphX, graphY, axis, version2 = 0, ink) {
4710
+ const yToX = yToXOf(axis);
4711
+ const index = getSpatialIndex(nodePositions, version2);
4712
+ const reach = (px) => Math.max(MIN_NODE_REACH_PX, px) / axis.scaleX;
4713
+ const queryReach = reach(ink?.maxHalfWidthPx ?? 0);
4714
+ const candidates = index.query(graphX, graphY, queryReach, queryReach / yToX);
4715
+ let hovered = null;
4716
+ let best = Infinity;
4717
+ for (const { nodeId, segmentIdx } of candidates) {
4718
+ const segments = nodePositions[nodeId];
4719
+ const dist = distanceToSegment(
4720
+ graphX,
4721
+ graphY * yToX,
4722
+ segments[segmentIdx].x,
4723
+ segments[segmentIdx].y * yToX,
4724
+ segments[segmentIdx + 1].x,
4725
+ segments[segmentIdx + 1].y * yToX
4726
+ );
4727
+ if (dist < best && dist < reach(ink?.halfWidthPx(nodeId) ?? 0)) {
4728
+ best = dist;
4729
+ hovered = nodeId;
4730
+ }
4731
+ }
4732
+ return hovered;
4733
+ }
4734
+ function segmentMeetsBox(a, b, box) {
4735
+ const dx = b.x - a.x;
4736
+ const dy = b.y - a.y;
4737
+ let t0 = 0;
4738
+ let t1 = 1;
4739
+ for (const [p, q] of [
4740
+ [-dx, a.x - box.x0],
4741
+ [dx, box.x1 - a.x],
4742
+ [-dy, a.y - box.y0],
4743
+ [dy, box.y1 - a.y]
4744
+ ]) {
4745
+ if (p === 0) {
4746
+ if (q < 0) {
4747
+ return false;
4748
+ }
4749
+ } else if (p < 0) {
4750
+ t0 = Math.max(t0, q / p);
4751
+ } else {
4752
+ t1 = Math.min(t1, q / p);
4753
+ }
4754
+ }
4755
+ return t0 <= t1;
4756
+ }
4757
+ function nodesUnderBox(nodePositions, t, version2 = 0) {
4758
+ const index = getSpatialIndex(nodePositions, version2);
4759
+ const screen = (p) => ({
4760
+ x: p.x * t.scaleX + t.translateX,
4761
+ y: p.y * t.scaleY + t.translateY
4762
+ });
4763
+ return (box) => {
4764
+ const hits = /* @__PURE__ */ new Set();
4765
+ const candidates = index.query(
4766
+ ((box.x0 + box.x1) / 2 - t.translateX) / t.scaleX,
4767
+ ((box.y0 + box.y1) / 2 - t.translateY) / t.scaleY,
4768
+ Math.abs((box.x1 - box.x0) / 2 / t.scaleX),
4769
+ Math.abs((box.y1 - box.y0) / 2 / t.scaleY)
4770
+ );
4771
+ for (const { nodeId, segmentIdx } of candidates) {
4772
+ const line = nodePositions[nodeId];
4773
+ if (!hits.has(nodeId) && segmentMeetsBox(
4774
+ screen(line[segmentIdx]),
4775
+ screen(line[segmentIdx + 1]),
4776
+ box
4777
+ )) {
4778
+ hits.add(nodeId);
4779
+ }
4780
+ }
4781
+ return hits;
4782
+ };
4783
+ }
4784
+ function findHoveredEdge(nodePositions, graph, graphX, graphY, axis, drawPaths, version2 = 0, deletions, hiddenEdges, deletionRoutes, stranded = false) {
4785
+ const yToX = yToXOf(axis);
4786
+ const edgeThreshold = 10 / axis.scaleX;
4787
+ const edgeIndex = getEdgeSpatialIndex(
4788
+ nodePositions,
4789
+ graph,
4790
+ drawPaths,
4791
+ axis,
4792
+ version2,
4793
+ deletions,
4794
+ deletionRoutes,
4795
+ stranded
4796
+ );
4797
+ const candidates = edgeIndex.query(
4798
+ graphX,
4799
+ graphY,
4800
+ edgeThreshold,
4801
+ edgeThreshold / yToX
4802
+ );
4803
+ let hovered = null;
4804
+ let best = edgeThreshold;
4805
+ for (const edgeIdx of candidates) {
4806
+ const edge = graph.edges[edgeIdx];
4807
+ const fromSegments = nodePositions[edge.from];
4808
+ const toSegments = nodePositions[edge.to];
4809
+ if (!fromSegments?.length || !toSegments?.length || hiddenEdges?.has(edgeIdx)) {
4810
+ continue;
4811
+ }
4812
+ const numPaths = edge.pathIds?.length ?? 0;
4813
+ const baseCurves = edgeIndex.getCurves(edgeIdx);
4814
+ if (!baseCurves) {
4815
+ continue;
4816
+ }
4817
+ let dist;
4818
+ if (!drawPaths || numPaths === 0) {
4819
+ dist = distanceToEdgeCurves(graphX, graphY * yToX, baseCurves, yToX);
4820
+ } else {
4821
+ let minDist = Infinity;
4822
+ for (const offset of pathRibbonOffsets(
4823
+ fromSegments,
4824
+ toSegments,
4825
+ numPaths,
4826
+ axis
4827
+ )) {
4828
+ const curves = offset.x === 0 && offset.y === 0 ? baseCurves : translateCurves(baseCurves, offset.x, offset.y);
4829
+ const d = distanceToEdgeCurves(graphX, graphY * yToX, curves, yToX);
4830
+ if (d < minDist) {
4831
+ minDist = d;
4832
+ }
4833
+ }
4834
+ dist = minDist;
4835
+ }
4836
+ if (dist < best) {
4837
+ best = dist;
4838
+ hovered = edgeIdx;
4839
+ }
4840
+ }
4841
+ return hovered;
4842
+ }
4843
+
4435
4844
  // src/walkEncoding.ts
4436
4845
  import { packAbgr as packAbgr3 } from "@jbrowse/core/util/colorBits";
4437
4846
  var WALK_FIELDS = [
@@ -4814,11 +5223,71 @@ function facetCells(names, by, domain = []) {
4814
5223
  return cells.includes(-1) || new Set(cells).size < cells.length ? wrap : { columns, count: rows.length * columns, cells };
4815
5224
  }
4816
5225
 
5226
+ // src/genes/exonOutline.ts
5227
+ var EXON_COLOR = "#daa520";
5228
+ var EXON_GAP_PX = 1;
5229
+ var EXON_LINE_PX = 2;
5230
+ var MIN_LANE_PX = 4;
5231
+ function exonStretches(pins, halfWidthPx, lift, pathOf = (d) => d) {
5232
+ const inkPx = (nodeId) => {
5233
+ const own = halfWidthPx(nodeId) * 2;
5234
+ return lift?.nodeIds.has(nodeId) ? Math.max(own, lift.walks.length * MIN_LANE_PX) : own;
5235
+ };
5236
+ return pins.flatMap(
5237
+ (pin) => pin.exonsByNode.map(({ nodeId, d }) => ({
5238
+ key: `${pin.gene.name}-${pin.gene.start}-${nodeId}`,
5239
+ d: pathOf(d),
5240
+ inner: inkPx(nodeId) + 2 * EXON_GAP_PX
5241
+ }))
5242
+ );
5243
+ }
5244
+ function exonOutlineTree(stretches, o) {
5245
+ if (stretches.length === 0) {
5246
+ return void 0;
5247
+ }
5248
+ const stroke = (s, color, width) => el("path", {
5249
+ d: s.d,
5250
+ fill: "none",
5251
+ stroke: color,
5252
+ "stroke-width": width,
5253
+ "stroke-linecap": "round",
5254
+ "stroke-linejoin": "round",
5255
+ "vector-effect": o.transform ? "non-scaling-stroke" : void 0
5256
+ });
5257
+ return el(
5258
+ "g",
5259
+ {},
5260
+ el(
5261
+ "mask",
5262
+ {
5263
+ id: o.id,
5264
+ maskUnits: "userSpaceOnUse",
5265
+ x: 0,
5266
+ y: 0,
5267
+ width: o.width,
5268
+ height: o.height
5269
+ },
5270
+ el(
5271
+ "g",
5272
+ { transform: o.transform },
5273
+ ...stretches.map((s) => stroke(s, "#fff", s.inner + 2 * EXON_LINE_PX)),
5274
+ ...stretches.map((s) => stroke(s, "#000", s.inner))
5275
+ )
5276
+ ),
5277
+ el("rect", {
5278
+ width: o.width,
5279
+ height: o.height,
5280
+ fill: EXON_COLOR,
5281
+ mask: `url(#${o.id})`
5282
+ })
5283
+ );
5284
+ }
5285
+
4817
5286
  // src/genes/genePins.ts
4818
5287
  function genePins(graph, genes, positions) {
4819
5288
  const byRefName = /* @__PURE__ */ new Map();
4820
5289
  for (const node of graph.nodes) {
4821
- if (isBackbone(node)) {
5290
+ if (isAnchored(node)) {
4822
5291
  const { refName } = node.stable;
4823
5292
  if (positions[node.id]?.length) {
4824
5293
  const entry = byRefName.get(refName) ?? byRefName.set(refName, { nodes: [], reach: 0 }).get(refName);
@@ -4832,17 +5301,18 @@ function genePins(graph, genes, positions) {
4832
5301
  }
4833
5302
  const pins = [];
4834
5303
  for (const gene of genes) {
4835
- const backbone = byRefName.get(gene.refName);
4836
- if (!backbone) {
5304
+ const onRefName = byRefName.get(gene.refName);
5305
+ if (!onRefName) {
4837
5306
  continue;
4838
5307
  }
4839
5308
  const parts = [];
4840
5309
  const exonsByNode = [];
5310
+ const nodeIds = /* @__PURE__ */ new Set();
4841
5311
  let at;
4842
5312
  let atDistance = Infinity;
4843
5313
  let covered = 0;
4844
5314
  const mid = (gene.start + gene.end) / 2;
4845
- const { nodes, reach } = backbone;
5315
+ const { nodes, reach } = onRefName;
4846
5316
  for (let i = firstNodeAtOrAfter(nodes, gene.start - reach); i < nodes.length && nodes[i].stable.start < gene.end; i++) {
4847
5317
  const node = nodes[i];
4848
5318
  const nodeStart = node.stable.start;
@@ -4851,6 +5321,7 @@ function genePins(graph, genes, positions) {
4851
5321
  continue;
4852
5322
  }
4853
5323
  const line = positions[node.id];
5324
+ nodeIds.add(node.id);
4854
5325
  covered += Math.min(nodeEnd, gene.end) - Math.max(nodeStart, gene.start);
4855
5326
  const nodeParts = [];
4856
5327
  for (const exon of gene.exons) {
@@ -4890,6 +5361,7 @@ function genePins(graph, genes, positions) {
4890
5361
  gene,
4891
5362
  exons: parts.join(""),
4892
5363
  exonsByNode,
5364
+ nodeIds,
4893
5365
  at,
4894
5366
  covered: covered / (gene.end - gene.start)
4895
5367
  });
@@ -4914,7 +5386,8 @@ function geneCoverageNote(pin) {
4914
5386
  const amount = (bp) => +(bp / per).toFixed(per === 1 ? 0 : 1);
4915
5387
  return `${amount(pin.covered * length)} of ${amount(length)} ${unit}`;
4916
5388
  }
4917
- function geneLabelCandidates(pins, screen, contigThickness) {
5389
+ function geneLabelCandidates(pins, screen, contigThickness, nodesUnder) {
5390
+ const ink = contigThickness / 2;
4918
5391
  return byExtent(pins, (pin) => pin.gene.end - pin.gene.start).map((pin) => {
4919
5392
  const { x, y } = screen(pin.at);
4920
5393
  const note = geneCoverageNote(pin);
@@ -4924,7 +5397,15 @@ function geneLabelCandidates(pins, screen, contigThickness) {
4924
5397
  y: y + contigThickness + GENE_PIN_DROP_PX,
4925
5398
  text: note ? `${pin.gene.name} \xB7 ${note}` : pin.gene.name,
4926
5399
  fallback: note ? `${pin.gene.name} \u2026` : void 0,
4927
- stack: GENE_STACK
5400
+ stack: GENE_STACK,
5401
+ fits: (box) => [
5402
+ ...nodesUnder({
5403
+ x0: box.x0 - ink,
5404
+ x1: box.x1 + ink,
5405
+ y0: box.y0 - ink,
5406
+ y1: box.y1 + ink
5407
+ })
5408
+ ].every((id) => pin.nodeIds.has(id))
4928
5409
  };
4929
5410
  });
4930
5411
  }
@@ -4985,7 +5466,16 @@ function layoutLabels(m) {
4985
5466
  take
4986
5467
  );
4987
5468
  const genes = placeLabels(
4988
- geneLabelCandidates(m.genePins, screen, m.contigThickness),
5469
+ geneLabelCandidates(
5470
+ m.genePins,
5471
+ screen,
5472
+ m.contigThickness,
5473
+ nodesUnderBox(
5474
+ m.nodePositions ?? {},
5475
+ { scaleX, scaleY, translateX, translateY },
5476
+ m.positionsVersion
5477
+ )
5478
+ ),
4989
5479
  frame,
4990
5480
  take
4991
5481
  );
@@ -5933,7 +6423,7 @@ function svgCanvas(width, height) {
5933
6423
  }
5934
6424
 
5935
6425
  // src/version.ts
5936
- var version = "5.1.0";
6426
+ var version = "5.2.0";
5937
6427
 
5938
6428
  // src/viewport.ts
5939
6429
  function axisScaleOf(scale, pixelRows) {
@@ -5983,11 +6473,7 @@ var SWATCH_PX = 18;
5983
6473
  var KEY_GAP_PX = 16;
5984
6474
  var STRIP_KEY_PX = 22;
5985
6475
  var FADED = "rgb(160,160,160)";
5986
- var EXON_COLOR = "#daa520";
5987
6476
  var GENE_INK2 = "#1c1c22";
5988
- var MIN_LANE_PX = 4;
5989
- var EXON_GAP_PX = 1;
5990
- var EXON_LINE_PX = 2;
5991
6477
  function esc(s) {
5992
6478
  return s.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;");
5993
6479
  }
@@ -6004,6 +6490,12 @@ function keySvg(id, x, y, stops, label, after, under, opacity = 1) {
6004
6490
  markup: `${defs}<g transform="translate(${x} ${y})" ${FONT}><rect y="5" width="${bar}" height="8" rx="2" ${fill}/><text x="${bar + 5}" y="13"><tspan font-weight="bold">${esc(label)}</tspan>${esc(after)}</text>${under ? `<text y="28">${esc(under)}</text>` : ""}</g>`
6005
6491
  };
6006
6492
  }
6493
+ function exonKeySvg(x, y) {
6494
+ return {
6495
+ width: SWATCH_PX + 5 + 4 * LABEL_CHAR_PX,
6496
+ markup: `<g transform="translate(${x} ${y})" ${FONT}><rect x="1" y="4" width="${SWATCH_PX - 2}" height="10" rx="4" fill="none" stroke="${EXON_COLOR}" stroke-width="2"/><text x="${SWATCH_PX + 5}" y="13">exon</text></g>`
6497
+ };
6498
+ }
6007
6499
  function walkKeySvg(id, x, y, walk, label, reference) {
6008
6500
  const key = walkKey(walk, reference);
6009
6501
  return keySvg(
@@ -6068,24 +6560,12 @@ function figureSvg(graph, layout, o = {}) {
6068
6560
  let masks = 0;
6069
6561
  function overlays(highlight, t, w, h) {
6070
6562
  const out = [];
6071
- const inkPx = (nodeId) => {
6072
- const own = ink.halfWidthPx(nodeId) * 2;
6073
- return highlight?.nodeIds.has(nodeId) ? Math.max(own, highlight.walks.length * MIN_LANE_PX) : own;
6074
- };
6075
- const stretches = pins.flatMap(
6076
- (pin) => pin.exonsByNode.map(({ nodeId, d }) => ({
6077
- d: screenPath(d, t),
6078
- inner: inkPx(nodeId) + 2 * EXON_GAP_PX
6079
- }))
6563
+ const outlines = exonOutlineTree(
6564
+ exonStretches(pins, ink.halfWidthPx, highlight, (d) => screenPath(d, t)),
6565
+ { id: `exons${masks++}`, width: w, height: h }
6080
6566
  );
6081
- if (stretches.length > 0) {
6082
- const id = `exons${masks++}`;
6083
- const stroke = (color, width2, d) => `<path d="${d}" fill="none" stroke="${color}" stroke-width="${width2}" stroke-linecap="round" stroke-linejoin="round"/>`;
6084
- out.push(
6085
- `<mask id="${id}" maskUnits="userSpaceOnUse" x="0" y="0" width="${w}" height="${h}">${stretches.map((s) => stroke("#fff", s.inner + 2 * EXON_LINE_PX, s.d)).join("")}${stretches.map((s) => stroke("#000", s.inner, s.d)).join(
6086
- ""
6087
- )}</mask><rect width="${w}" height="${h}" fill="${EXON_COLOR}" mask="url(#${id})"/>`
6088
- );
6567
+ if (outlines) {
6568
+ out.push(serializeEl(outlines));
6089
6569
  }
6090
6570
  const screen = (p) => ({
6091
6571
  x: p.x * t.scaleX + t.translateX,
@@ -6093,7 +6573,12 @@ function figureSvg(graph, layout, o = {}) {
6093
6573
  });
6094
6574
  const frame = { width: w, height: h };
6095
6575
  for (const { item: pin, x, y, w: cw, text } of placeLabels(
6096
- geneLabelCandidates(pins, screen, contigThickness),
6576
+ geneLabelCandidates(
6577
+ pins,
6578
+ screen,
6579
+ contigThickness,
6580
+ nodesUnderBox(layout.nodePositions, t)
6581
+ ),
6097
6582
  frame,
6098
6583
  occupancy(frame)
6099
6584
  )) {
@@ -6259,6 +6744,9 @@ function figureSvg(graph, layout, o = {}) {
6259
6744
  )
6260
6745
  );
6261
6746
  }
6747
+ if (pins.some((pin) => pin.exonsByNode.length > 0)) {
6748
+ push(exonKeySvg(x, 2));
6749
+ }
6262
6750
  const header = parts.length > 0 ? FACET_TITLE_PX : 0;
6263
6751
  const usable = width - 2 * FIT_PADDING;
6264
6752
  const drawn = pixelRows || !(bounds.w > 0) ? bounds.h : bounds.h * usable / bounds.w;
@@ -6625,7 +7113,9 @@ var NodeLimitError = class extends Error {
6625
7113
  super(
6626
7114
  `this ${windowBp.toLocaleString()} bp window reads more than nodeLimit (${limit.toLocaleString()}) graph nodes; zoom in to about ${fitsBp.toLocaleString()} bp or raise nodeLimit`
6627
7115
  );
7116
+ this.fitsBp = fitsBp;
6628
7117
  this.name = "NodeLimitError";
7118
+ this.regionTooLarge = true;
6629
7119
  }
6630
7120
  };
6631
7121
  function haplotypePrefix(name) {
@@ -6770,10 +7260,6 @@ export {
6770
7260
  BUBBLE_SPREADS,
6771
7261
  ROW_HEIGHT_PX,
6772
7262
  svgPath,
6773
- curveBounds,
6774
- translateCurves,
6775
- yToXOf,
6776
- pathRibbonOffsets,
6777
7263
  deletionEdges,
6778
7264
  panSNSample,
6779
7265
  panSNHaplotype,
@@ -6809,7 +7295,6 @@ export {
6809
7295
  nodeWidthPx,
6810
7296
  maxNodeWidthPx,
6811
7297
  nodeInk,
6812
- baseEdgeCurves,
6813
7298
  LIFT_BACKDROP_CSS,
6814
7299
  REFERENCE_RAMP_MAX_HUE,
6815
7300
  computeReferenceRamp,
@@ -6817,6 +7302,8 @@ export {
6817
7302
  getNodeColor,
6818
7303
  buildGeometry,
6819
7304
  Canvas2DRenderer,
7305
+ findHoveredNode,
7306
+ findHoveredEdge,
6820
7307
  WALK_FIELDS,
6821
7308
  WALK_SCHEMES,
6822
7309
  resolveEncoding,
@@ -6837,6 +7324,9 @@ export {
6837
7324
  facetGrid,
6838
7325
  facetSettingOf,
6839
7326
  facetCells,
7327
+ EXON_COLOR,
7328
+ exonStretches,
7329
+ exonOutlineTree,
6840
7330
  genePins,
6841
7331
  HALO_FACTOR,
6842
7332
  LEGEND_INSET_PX,
@@ -21,7 +21,7 @@ import {
21
21
  placeRowGenes,
22
22
  refNameBinding,
23
23
  walkRows
24
- } from "../chunks/chunk-Y2MDXT4O.js";
24
+ } from "../chunks/chunk-ZMVFSZI5.js";
25
25
 
26
26
  // src/cli/figure.ts
27
27
  import { access, open, readFile, writeFile } from "node:fs/promises";
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ import {
4
4
  COLOR_SCHEMES,
5
5
  Canvas2DRenderer,
6
6
  DISAGREES,
7
+ EXON_COLOR,
7
8
  FACET_GAP_PX,
8
9
  FACET_PAD_PX,
9
10
  FACET_TITLE_PX,
@@ -35,14 +36,12 @@ import {
35
36
  assemblyWalk,
36
37
  axisScaleOf,
37
38
  backboneAssembly,
38
- baseEdgeCurves,
39
39
  buildGeometry,
40
40
  clampZoom,
41
41
  coalesceRuns,
42
42
  computeColorSchemeRange,
43
43
  computeReferenceRamp,
44
44
  contains,
45
- curveBounds,
46
45
  cutGbzRegion,
47
46
  cutWindowGFA,
48
47
  deletionEdges,
@@ -52,6 +51,8 @@ import {
52
51
  encodingSwatchCss,
53
52
  engineKey,
54
53
  engineRequest,
54
+ exonOutlineTree,
55
+ exonStretches,
55
56
  facetCells,
56
57
  facetGrid,
57
58
  facetLifts,
@@ -59,6 +60,8 @@ import {
59
60
  featuresOnBackbone,
60
61
  figureSvg,
61
62
  filterSamples,
63
+ findHoveredEdge,
64
+ findHoveredNode,
62
65
  fitTransform,
63
66
  forceLayout,
64
67
  formatBp,
@@ -91,7 +94,6 @@ import {
91
94
  parseRegion,
92
95
  pathColorsLegible,
93
96
  pathLegend,
94
- pathRibbonOffsets,
95
97
  placeRowGenes,
96
98
  rampHue,
97
99
  rampHueCss,
@@ -118,7 +120,6 @@ import {
118
120
  stripRowAt,
119
121
  svgCanvas,
120
122
  svgPath,
121
- translateCurves,
122
123
  trimOrigins,
123
124
  unitTicks,
124
125
  version,
@@ -137,372 +138,12 @@ import {
137
138
  walkStripFrame,
138
139
  walkStripLabelsTree,
139
140
  wellKnownSample,
140
- yToXOf,
141
141
  zoomAbout
142
- } from "./chunks/chunk-Y2MDXT4O.js";
142
+ } from "./chunks/chunk-ZMVFSZI5.js";
143
143
 
144
144
  // src/index.ts
145
145
  import { getDpr } from "@jbrowse/render-core/canvas2dUtils";
146
146
 
147
- // src/util/SpatialIndex.ts
148
- var FALLBACK_CELL_SIZE = 50;
149
- function meanExtentCellSize(extentTotal, count) {
150
- return count > 0 ? Math.max(1, extentTotal / count) : FALLBACK_CELL_SIZE;
151
- }
152
- function rowPitchOf(boxes) {
153
- const distinct = /* @__PURE__ */ new Set();
154
- let min = Infinity;
155
- let max = -Infinity;
156
- for (const box of boxes) {
157
- distinct.add(box.minY);
158
- min = Math.min(min, box.minY);
159
- max = Math.max(max, box.minY);
160
- }
161
- return distinct.size > 1 ? (max - min) / (distinct.size - 1) : void 0;
162
- }
163
- function cellSizeFrom(boxes, extentX, extentY) {
164
- const x = meanExtentCellSize(extentX, boxes.length);
165
- return {
166
- x,
167
- y: extentY > 0 ? meanExtentCellSize(extentY, boxes.length) : rowPitchOf(boxes) ?? x
168
- };
169
- }
170
- function resolveCellSize(given, boxes, extentX, extentY) {
171
- return given === void 0 ? cellSizeFrom(boxes, extentX, extentY) : typeof given === "number" ? { x: given, y: given } : given;
172
- }
173
- function getOrCreateCell(cells, cx, cy) {
174
- let row = cells.get(cx);
175
- if (!row) {
176
- row = /* @__PURE__ */ new Map();
177
- cells.set(cx, row);
178
- }
179
- let cell = row.get(cy);
180
- if (!cell) {
181
- cell = [];
182
- row.set(cy, cell);
183
- }
184
- return cell;
185
- }
186
- function addToGrid(cells, cellSize, minX, minY, maxX, maxY, entry) {
187
- const cx0 = Math.floor(minX / cellSize.x);
188
- const cx1 = Math.floor(maxX / cellSize.x);
189
- const cy0 = Math.floor(minY / cellSize.y);
190
- const cy1 = Math.floor(maxY / cellSize.y);
191
- for (let cx = cx0; cx <= cx1; cx++) {
192
- for (let cy = cy0; cy <= cy1; cy++) {
193
- getOrCreateCell(cells, cx, cy).push(entry);
194
- }
195
- }
196
- }
197
- function queryGrid(cells, cellSize, x, y, radiusX, radiusY) {
198
- const cx0 = Math.floor((x - radiusX) / cellSize.x);
199
- const cx1 = Math.floor((x + radiusX) / cellSize.x);
200
- const cy0 = Math.floor((y - radiusY) / cellSize.y);
201
- const cy1 = Math.floor((y + radiusY) / cellSize.y);
202
- const results = [];
203
- const seen = /* @__PURE__ */ new Set();
204
- for (let cx = cx0; cx <= cx1; cx++) {
205
- const row = cells.get(cx);
206
- if (row) {
207
- for (let cy = cy0; cy <= cy1; cy++) {
208
- const cell = row.get(cy);
209
- if (cell) {
210
- for (const entry of cell) {
211
- if (!seen.has(entry)) {
212
- seen.add(entry);
213
- results.push(entry);
214
- }
215
- }
216
- }
217
- }
218
- }
219
- }
220
- return results;
221
- }
222
- var SpatialIndex = class {
223
- constructor(nodePositions, cellSize) {
224
- this.cells = /* @__PURE__ */ new Map();
225
- const boxes = [];
226
- let extentX = 0;
227
- let extentY = 0;
228
- for (const [nodeId, segments] of Object.entries(nodePositions)) {
229
- for (let i = 0; i < segments.length - 1; i++) {
230
- const seg = segments[i];
231
- const next = segments[i + 1];
232
- const minX = Math.min(seg.x, next.x);
233
- const minY = Math.min(seg.y, next.y);
234
- const maxX = Math.max(seg.x, next.x);
235
- const maxY = Math.max(seg.y, next.y);
236
- extentX += maxX - minX;
237
- extentY += maxY - minY;
238
- boxes.push({ entry: { nodeId, segmentIdx: i }, minX, minY, maxX, maxY });
239
- }
240
- }
241
- this.cellSize = resolveCellSize(cellSize, boxes, extentX, extentY);
242
- for (const b of boxes) {
243
- addToGrid(
244
- this.cells,
245
- this.cellSize,
246
- b.minX,
247
- b.minY,
248
- b.maxX,
249
- b.maxY,
250
- b.entry
251
- );
252
- }
253
- }
254
- query(x, y, radiusX, radiusY = radiusX) {
255
- return queryGrid(this.cells, this.cellSize, x, y, radiusX, radiusY);
256
- }
257
- };
258
- var EdgeSpatialIndex = class {
259
- constructor(nodePositions, graph, drawPaths, axis, cellSize, deletions, version2 = 0, routes2, stranded = false) {
260
- this.cells = /* @__PURE__ */ new Map();
261
- this.edgeCurves = baseEdgeCurves(
262
- nodePositions,
263
- graph,
264
- axis,
265
- deletions,
266
- version2,
267
- routes2,
268
- stranded
269
- );
270
- const boxes = [];
271
- let extentX = 0;
272
- let extentY = 0;
273
- for (let ei = 0; ei < graph.edges.length; ei++) {
274
- const edge = graph.edges[ei];
275
- const fromSegments = nodePositions[edge.from];
276
- const toSegments = nodePositions[edge.to];
277
- const curves = this.edgeCurves.get(ei);
278
- if (fromSegments?.length && toSegments?.length && curves) {
279
- let { minX, minY, maxX, maxY } = curveBounds(curves);
280
- const numPaths = edge.pathIds?.length ?? 0;
281
- if (drawPaths && numPaths > 0) {
282
- let padX = 0;
283
- let padY = 0;
284
- for (const o of pathRibbonOffsets(
285
- fromSegments,
286
- toSegments,
287
- numPaths,
288
- axis
289
- )) {
290
- padX = Math.max(padX, Math.abs(o.x));
291
- padY = Math.max(padY, Math.abs(o.y));
292
- }
293
- minX -= padX;
294
- minY -= padY;
295
- maxX += padX;
296
- maxY += padY;
297
- }
298
- extentX += maxX - minX;
299
- extentY += maxY - minY;
300
- boxes.push({ ei, minX, minY, maxX, maxY });
301
- }
302
- }
303
- this.cellSize = resolveCellSize(cellSize, boxes, extentX, extentY);
304
- for (const b of boxes) {
305
- addToGrid(this.cells, this.cellSize, b.minX, b.minY, b.maxX, b.maxY, b.ei);
306
- }
307
- }
308
- getCurves(ei) {
309
- return this.edgeCurves.get(ei);
310
- }
311
- query(x, y, radiusX, radiusY = radiusX) {
312
- return queryGrid(this.cells, this.cellSize, x, y, radiusX, radiusY);
313
- }
314
- };
315
-
316
- // src/util/hitDetection.ts
317
- function distanceToSegment(px, py, x1, y1, x2, y2) {
318
- const dx = x2 - x1;
319
- const dy = y2 - y1;
320
- const lenSq = dx * dx + dy * dy;
321
- if (lenSq === 0) {
322
- return Math.hypot(px - x1, py - y1);
323
- }
324
- const t = Math.max(0, Math.min(1, ((px - x1) * dx + (py - y1) * dy) / lenSq));
325
- return Math.hypot(px - (x1 + t * dx), py - (y1 + t * dy));
326
- }
327
- function distanceToCubicBezier(px, py, x1, y1, cx1, cy1, cx2, cy2, x2, y2) {
328
- let minDist = Infinity;
329
- const samples = 20;
330
- let lastX = x1;
331
- let lastY = y1;
332
- for (let i = 1; i <= samples; i++) {
333
- const t = i / samples;
334
- const u = 1 - t;
335
- const bx = u * u * u * x1 + 3 * u * u * t * cx1 + 3 * u * t * t * cx2 + t * t * t * x2;
336
- const by = u * u * u * y1 + 3 * u * u * t * cy1 + 3 * u * t * t * cy2 + t * t * t * y2;
337
- minDist = Math.min(minDist, distanceToSegment(px, py, lastX, lastY, bx, by));
338
- lastX = bx;
339
- lastY = by;
340
- }
341
- return minDist;
342
- }
343
- function distanceToBezierCurve(px, py, c) {
344
- return distanceToCubicBezier(
345
- px,
346
- py,
347
- c.x0,
348
- c.y0,
349
- c.cx0,
350
- c.cy0,
351
- c.cx1,
352
- c.cy1,
353
- c.x1,
354
- c.y1
355
- );
356
- }
357
- function distanceToEdgeCurves(px, py, curves, yToX = 1) {
358
- let minDist = Infinity;
359
- for (const c of curves) {
360
- const d = distanceToBezierCurve(
361
- px,
362
- py,
363
- yToX === 1 ? c : {
364
- x0: c.x0,
365
- y0: c.y0 * yToX,
366
- cx0: c.cx0,
367
- cy0: c.cy0 * yToX,
368
- cx1: c.cx1,
369
- cy1: c.cy1 * yToX,
370
- x1: c.x1,
371
- y1: c.y1 * yToX
372
- }
373
- );
374
- if (d < minDist) {
375
- minDist = d;
376
- }
377
- }
378
- return minDist;
379
- }
380
- var nodeCache = /* @__PURE__ */ new WeakMap();
381
- function getSpatialIndex(nodePositions, version2) {
382
- const cached = nodeCache.get(nodePositions);
383
- if (cached?.version === version2) {
384
- return cached.index;
385
- }
386
- const index = new SpatialIndex(nodePositions);
387
- nodeCache.set(nodePositions, { version: version2, index });
388
- return index;
389
- }
390
- var edgeCache = /* @__PURE__ */ new WeakMap();
391
- function getEdgeSpatialIndex(nodePositions, graph, drawPaths, axis, version2, deletions, routes2, stranded = false) {
392
- const cached = edgeCache.get(nodePositions);
393
- if (cached?.graph === graph && cached.drawPaths === drawPaths && cached.scaleX === axis.scaleX && cached.scaleY === axis.scaleY && cached.version === version2 && cached.deletions === deletions && cached.routes === routes2 && cached.stranded === stranded) {
394
- return cached.index;
395
- }
396
- const index = new EdgeSpatialIndex(
397
- nodePositions,
398
- graph,
399
- drawPaths,
400
- axis,
401
- void 0,
402
- deletions,
403
- version2,
404
- routes2,
405
- stranded
406
- );
407
- edgeCache.set(nodePositions, {
408
- graph,
409
- drawPaths,
410
- scaleX: axis.scaleX,
411
- scaleY: axis.scaleY,
412
- version: version2,
413
- index,
414
- deletions,
415
- routes: routes2,
416
- stranded
417
- });
418
- return index;
419
- }
420
- var MIN_NODE_REACH_PX = 5;
421
- function findHoveredNode(nodePositions, graphX, graphY, axis, version2 = 0, ink) {
422
- const yToX = yToXOf(axis);
423
- const index = getSpatialIndex(nodePositions, version2);
424
- const reach = (px) => Math.max(MIN_NODE_REACH_PX, px) / axis.scaleX;
425
- const queryReach = reach(ink?.maxHalfWidthPx ?? 0);
426
- const candidates = index.query(graphX, graphY, queryReach, queryReach / yToX);
427
- let hovered = null;
428
- let best = Infinity;
429
- for (const { nodeId, segmentIdx } of candidates) {
430
- const segments = nodePositions[nodeId];
431
- const dist = distanceToSegment(
432
- graphX,
433
- graphY * yToX,
434
- segments[segmentIdx].x,
435
- segments[segmentIdx].y * yToX,
436
- segments[segmentIdx + 1].x,
437
- segments[segmentIdx + 1].y * yToX
438
- );
439
- if (dist < best && dist < reach(ink?.halfWidthPx(nodeId) ?? 0)) {
440
- best = dist;
441
- hovered = nodeId;
442
- }
443
- }
444
- return hovered;
445
- }
446
- function findHoveredEdge(nodePositions, graph, graphX, graphY, axis, drawPaths, version2 = 0, deletions, hiddenEdges, deletionRoutes, stranded = false) {
447
- const yToX = yToXOf(axis);
448
- const edgeThreshold = 10 / axis.scaleX;
449
- const edgeIndex = getEdgeSpatialIndex(
450
- nodePositions,
451
- graph,
452
- drawPaths,
453
- axis,
454
- version2,
455
- deletions,
456
- deletionRoutes,
457
- stranded
458
- );
459
- const candidates = edgeIndex.query(
460
- graphX,
461
- graphY,
462
- edgeThreshold,
463
- edgeThreshold / yToX
464
- );
465
- let hovered = null;
466
- let best = edgeThreshold;
467
- for (const edgeIdx of candidates) {
468
- const edge = graph.edges[edgeIdx];
469
- const fromSegments = nodePositions[edge.from];
470
- const toSegments = nodePositions[edge.to];
471
- if (!fromSegments?.length || !toSegments?.length || hiddenEdges?.has(edgeIdx)) {
472
- continue;
473
- }
474
- const numPaths = edge.pathIds?.length ?? 0;
475
- const baseCurves = edgeIndex.getCurves(edgeIdx);
476
- if (!baseCurves) {
477
- continue;
478
- }
479
- let dist;
480
- if (!drawPaths || numPaths === 0) {
481
- dist = distanceToEdgeCurves(graphX, graphY * yToX, baseCurves, yToX);
482
- } else {
483
- let minDist = Infinity;
484
- for (const offset of pathRibbonOffsets(
485
- fromSegments,
486
- toSegments,
487
- numPaths,
488
- axis
489
- )) {
490
- const curves = offset.x === 0 && offset.y === 0 ? baseCurves : translateCurves(baseCurves, offset.x, offset.y);
491
- const d = distanceToEdgeCurves(graphX, graphY * yToX, curves, yToX);
492
- if (d < minDist) {
493
- minDist = d;
494
- }
495
- }
496
- dist = minDist;
497
- }
498
- if (dist < best) {
499
- best = dist;
500
- hovered = edgeIdx;
501
- }
502
- }
503
- return hovered;
504
- }
505
-
506
147
  // src/util/wheelZoom.ts
507
148
  var ZOOM_PER_NOTCH = 1.1;
508
149
  var NOTCH_PX = 100;
@@ -1797,6 +1438,7 @@ export {
1797
1438
  BUBBLE_SPREADS,
1798
1439
  COLOR_SCHEMES,
1799
1440
  Canvas2DRenderer,
1441
+ EXON_COLOR,
1800
1442
  FACET_GAP_PX,
1801
1443
  FACET_PAD_PX,
1802
1444
  FACET_TITLE_PX,
@@ -1855,6 +1497,8 @@ export {
1855
1497
  encodingSwatchCss,
1856
1498
  engineKey,
1857
1499
  engineRequest,
1500
+ exonOutlineTree,
1501
+ exonStretches,
1858
1502
  facetCells,
1859
1503
  facetGrid,
1860
1504
  facetLifts,
@@ -4,7 +4,9 @@ export declare class NoReferenceSampleError extends Error {
4
4
  constructor(anchor: string, referenceSamples: string[]);
5
5
  }
6
6
  export declare class NodeLimitError extends Error {
7
+ readonly fitsBp: number;
7
8
  name: string;
9
+ readonly regionTooLarge = true;
8
10
  constructor(limit: number, windowBp: number, fitsBp: number);
9
11
  }
10
12
  export declare function haplotypePrefix(name: Pick<PathName, 'sample' | 'haplotype'>): string;
@@ -0,0 +1,18 @@
1
+ import type { El } from '../el.js';
2
+ import type { GenePin } from './genePins.js';
3
+ export declare const EXON_COLOR = "#daa520";
4
+ export interface ExonStretch {
5
+ key: string;
6
+ d: string;
7
+ inner: number;
8
+ }
9
+ export declare function exonStretches(pins: GenePin[], halfWidthPx: (nodeId: string) => number, lift?: {
10
+ nodeIds: ReadonlySet<string>;
11
+ walks: readonly unknown[];
12
+ }, pathOf?: (d: string) => string): ExonStretch[];
13
+ export declare function exonOutlineTree(stretches: ExonStretch[], o: {
14
+ id: string;
15
+ width: number;
16
+ height: number;
17
+ transform?: string;
18
+ }): El | undefined;
@@ -17,6 +17,7 @@ export interface GenePin {
17
17
  nodeId: string;
18
18
  d: string;
19
19
  }[];
20
+ nodeIds: ReadonlySet<string>;
20
21
  at: NodeSegment;
21
22
  covered: number;
22
23
  }
@@ -68,6 +68,8 @@ export { ROW_HEIGHT_PX } from './layout/rowSpacing.js';
68
68
  export { HALO_FACTOR, LEGEND_INSET_PX, geneCoverageNote, layoutLabels, } from './labelLayout.js';
69
69
  export type { LabelLayout, LabelLayoutSource } from './labelLayout.js';
70
70
  export { formatBp } from './graphLabels.js';
71
+ export { EXON_COLOR, exonOutlineTree, exonStretches } from './genes/exonOutline.js';
72
+ export type { ExonStretch } from './genes/exonOutline.js';
71
73
  export { genePins } from './genes/genePins.js';
72
74
  export type { GeneModel, GenePin } from './genes/genePins.js';
73
75
  export { WELL_KNOWN_SAMPLES, assemblyWalk, backboneAssembly, featuresOnBackbone, graphBackbone, refNameBinding, referenceLabel, wellKnownSample, } from './reference.js';
@@ -3,7 +3,7 @@ import type { DeletionEdge } from './deletionEdges.js';
3
3
  import type { GenePin } from './genes/genePins.js';
4
4
  import type { GraphLabel } from './graphLabels.js';
5
5
  import type { DeletionRoutes } from './layout/deletionRoutes.js';
6
- import type { PlacedLabel } from './overlayLabels.js';
6
+ import type { Box, PlacedLabel } from './overlayLabels.js';
7
7
  import type { AlleleDeletion, NodeSegment } from './types.js';
8
8
  import type { AxisScale } from './util/geometry.js';
9
9
  export declare const HALO_FACTOR = 3.4;
@@ -55,12 +55,13 @@ export declare function geneLabelCandidates(pins: GenePin[], screen: (p: {
55
55
  }) => {
56
56
  x: number;
57
57
  y: number;
58
- }, contigThickness: number): {
58
+ }, contigThickness: number, nodesUnder: (box: Box) => ReadonlySet<string>): {
59
59
  item: GenePin;
60
60
  x: number;
61
61
  y: number;
62
62
  text: string;
63
63
  fallback: string | undefined;
64
64
  stack: number;
65
+ fits: (box: Box) => boolean;
65
66
  }[];
66
67
  export declare function layoutLabels(m: LabelLayoutSource): LabelLayout;
@@ -14,6 +14,7 @@ export interface LabelCandidate<T> {
14
14
  text: string;
15
15
  stack?: number;
16
16
  fallback?: string;
17
+ fits?: (box: Box) => boolean;
17
18
  }
18
19
  export interface PlacedLabel<T> extends LabelCandidate<T> {
19
20
  w: number;
@@ -1,4 +1,5 @@
1
1
  import type { DeletionRoutes } from '../layout/deletionRoutes.js';
2
+ import type { Box } from '../overlayLabels.js';
2
3
  import type { Graph, NodeSegment } from '../types.js';
3
4
  import type { AxisScale } from './geometry.js';
4
5
  export declare function distanceToSegment(px: number, py: number, x1: number, y1: number, x2: number, y2: number): number;
@@ -8,4 +9,10 @@ export interface NodeInk {
8
9
  maxHalfWidthPx: number;
9
10
  }
10
11
  export declare function findHoveredNode(nodePositions: Record<string, NodeSegment[]>, graphX: number, graphY: number, axis: AxisScale, version?: number, ink?: NodeInk): string | null;
12
+ export declare function nodesUnderBox(nodePositions: Record<string, NodeSegment[]>, t: {
13
+ scaleX: number;
14
+ scaleY: number;
15
+ translateX: number;
16
+ translateY: number;
17
+ }, version?: number): (box: Box) => Set<string>;
11
18
  export declare function findHoveredEdge(nodePositions: Record<string, NodeSegment[]>, graph: Graph, graphX: number, graphY: number, axis: AxisScale, drawPaths: boolean, version?: number, deletions?: Map<number, string[]>, hiddenEdges?: ReadonlySet<number>, deletionRoutes?: DeletionRoutes, stranded?: boolean): number | null;
@@ -1 +1 @@
1
- export declare const version = "5.1.0";
1
+ export declare const version = "5.2.0";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jbrowse/bandage-core",
3
- "version": "5.1.0",
3
+ "version": "5.2.0",
4
4
  "description": "Pangenome graph layout and drawing with no host: GFA in, the Bandage force-directed engine and reference-anchored layouts, a Canvas2D renderer, hit testing and labels out",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {