@jbrowse/bandage-core 7.1.0 → 7.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.
@@ -0,0 +1,42 @@
1
+ import {
2
+ stressLayout
3
+ } from "./chunk-UNOE2NDN.js";
4
+ import {
5
+ loadBandage
6
+ } from "./chunk-7XHHVYG7.js";
7
+
8
+ // src/layoutEngines.ts
9
+ var LAYOUT_ENGINES = [
10
+ {
11
+ value: "fmmm",
12
+ label: "Bandage (FMMM)",
13
+ description: "Bandage's force-directed layout: OGDF's fast multipole multilevel method, compiled to wasm."
14
+ },
15
+ {
16
+ value: "stress",
17
+ label: "Stress (experimental)",
18
+ description: "Stress layout by stochastic gradient descent in plain JS: distances along the graph become distances on the page, so the reference reads straight and a repeat loop reads as a ring."
19
+ }
20
+ ];
21
+ var LAYOUT_ENGINE_VALUES = LAYOUT_ENGINES.map((e) => e.value);
22
+ function isLayoutEngineKind(value) {
23
+ return LAYOUT_ENGINE_VALUES.includes(value);
24
+ }
25
+ var layoutEngine = async (request) => {
26
+ if (request.options.engine === "stress") {
27
+ const start2 = performance.now();
28
+ const result2 = stressLayout(request);
29
+ return { result: result2, duration: performance.now() - start2 };
30
+ }
31
+ const bandage = await loadBandage();
32
+ const start = performance.now();
33
+ const result = bandage.computeLayout(request.graph, request.options);
34
+ return { result, duration: performance.now() - start };
35
+ };
36
+
37
+ export {
38
+ LAYOUT_ENGINES,
39
+ LAYOUT_ENGINE_VALUES,
40
+ isLayoutEngineKind,
41
+ layoutEngine
42
+ };
@@ -14,7 +14,7 @@ import {
14
14
  routeNodes,
15
15
  takeRoutes,
16
16
  withDeletionRoutes
17
- } from "./chunk-YZ5CCEDH.js";
17
+ } from "./chunk-JVLURPFW.js";
18
18
  import {
19
19
  anchorGraph
20
20
  } from "./chunk-ST2IBUAN.js";
@@ -73,6 +73,7 @@ function takeRoutes(positions, routes) {
73
73
  }
74
74
 
75
75
  export {
76
+ ROUTE_ID,
76
77
  withDeletionRoutes,
77
78
  routeNodes,
78
79
  takeRoutes
@@ -0,0 +1,664 @@
1
+ import {
2
+ ROUTE_ID
3
+ } from "./chunk-JVLURPFW.js";
4
+
5
+ // src/layout/stressPlacement.ts
6
+ var MIN_DEPTH = 12;
7
+ var LANE_GAP = 15;
8
+ var CLEAR = LANE_GAP * 0.7;
9
+ var TIP_ANGLE = 25 * Math.PI / 180;
10
+ var ROUTE_BOW = 0.25;
11
+ var ROUTE_CAP = 150;
12
+ var TEARDROP_TILTS = [0, -0.35, 0.35, -0.7, 0.7];
13
+ var SIDES = [0, 1];
14
+ function other(side) {
15
+ return side ? 0 : 1;
16
+ }
17
+ function unit(a, b) {
18
+ const dx = b.x - a.x;
19
+ const dy = b.y - a.y;
20
+ const m = Math.hypot(dx, dy) || 1;
21
+ return { x: dx / m, y: dy / m };
22
+ }
23
+ function lerp(a, b, f) {
24
+ return { x: a.x + (b.x - a.x) * f, y: a.y + (b.y - a.y) * f };
25
+ }
26
+ function centroidOf(pts) {
27
+ let x = 0;
28
+ let y = 0;
29
+ for (const p of pts) {
30
+ x += p.x;
31
+ y += p.y;
32
+ }
33
+ return { x: x / pts.length, y: y / pts.length };
34
+ }
35
+ function arcLengths(pts) {
36
+ const arc = new Float64Array(pts.length);
37
+ for (let i = 1; i < pts.length; i++) {
38
+ arc[i] = arc[i - 1] + Math.hypot(pts[i].x - pts[i - 1].x, pts[i].y - pts[i - 1].y);
39
+ }
40
+ return arc;
41
+ }
42
+ function polylineLength(pts) {
43
+ return arcLengths(pts)[pts.length - 1] ?? 0;
44
+ }
45
+ function arcAngle(ratio) {
46
+ let lo = 1e-6;
47
+ let hi = 2 * Math.PI - 1e-6;
48
+ for (let i = 0; i < 60; i++) {
49
+ const mid = (lo + hi) / 2;
50
+ if (mid / (2 * Math.sin(mid / 2)) < ratio) {
51
+ lo = mid;
52
+ } else {
53
+ hi = mid;
54
+ }
55
+ }
56
+ return (lo + hi) / 2;
57
+ }
58
+ function archLength(c, s) {
59
+ if (c < 1e-9) {
60
+ return Math.PI * s;
61
+ }
62
+ const R = (c * c / 4 + s * s) / (2 * s);
63
+ const theta = 2 * Math.asin(Math.min(1, c / (2 * R)));
64
+ return R * (s > R ? 2 * Math.PI - theta : theta);
65
+ }
66
+ function arcFlat(c, length, h, drop) {
67
+ const arc = length - 2 * h;
68
+ const theta = c < 1e-9 ? 2 * Math.PI : arcAngle(arc / c);
69
+ const R = arc / theta;
70
+ const cos = Math.cos(theta / 2);
71
+ const cu = c / 2;
72
+ const cv = drop + h - R * cos;
73
+ return {
74
+ at: (t) => {
75
+ if (t <= h) {
76
+ return { u: 0, v: drop + t };
77
+ }
78
+ if (t >= length - h) {
79
+ return { u: c, v: drop + length - t };
80
+ }
81
+ const a = (t - h) / R - theta / 2;
82
+ return { u: cu + R * Math.sin(a), v: cv + R * Math.cos(a) };
83
+ },
84
+ apex: drop + h + R * (1 - cos),
85
+ length
86
+ };
87
+ }
88
+ function tilted(flat, c, angle) {
89
+ if (!angle) {
90
+ return flat;
91
+ }
92
+ const cos = Math.cos(angle);
93
+ const sin = Math.sin(angle);
94
+ return {
95
+ ...flat,
96
+ at: (t) => {
97
+ const { u, v } = flat.at(t);
98
+ const du = u - c / 2;
99
+ return { u: c / 2 + du * cos - v * sin, v: du * sin + v * cos };
100
+ }
101
+ };
102
+ }
103
+ function lineFlat(u0, v0, du, dv, length) {
104
+ return {
105
+ at: (t) => ({ u: u0 + du * t, v: v0 + dv * t }),
106
+ apex: v0 + Math.max(0, dv) * length,
107
+ length
108
+ };
109
+ }
110
+ var Curve = class {
111
+ constructor(pts, normals, straight = false) {
112
+ this.pts = pts;
113
+ this.normals = normals;
114
+ this.straight = straight;
115
+ this.arc = arcLengths(pts);
116
+ this.total = this.arc[pts.length - 1] ?? 0;
117
+ }
118
+ segment(t) {
119
+ const { arc } = this;
120
+ let lo = 1;
121
+ let hi = arc.length - 1;
122
+ while (lo < hi) {
123
+ const mid = lo + hi >> 1;
124
+ if (arc[mid] < t) {
125
+ lo = mid + 1;
126
+ } else {
127
+ hi = mid;
128
+ }
129
+ }
130
+ return lo;
131
+ }
132
+ fraction(i, t) {
133
+ const span = this.arc[i] - this.arc[i - 1];
134
+ return span > 0 ? (t - this.arc[i - 1]) / span : 0;
135
+ }
136
+ pointAt(t) {
137
+ const { pts } = this;
138
+ const last = pts.length - 1;
139
+ if (last === 0) {
140
+ return { ...pts[0] };
141
+ }
142
+ if (t <= 0) {
143
+ const d = unit(pts[0], pts[1]);
144
+ return { x: pts[0].x + d.x * t, y: pts[0].y + d.y * t };
145
+ }
146
+ if (t >= this.total) {
147
+ const d = unit(pts[last - 1], pts[last]);
148
+ const r = t - this.total;
149
+ return { x: pts[last].x + d.x * r, y: pts[last].y + d.y * r };
150
+ }
151
+ const i = this.segment(t);
152
+ return lerp(pts[i - 1], pts[i], this.fraction(i, t));
153
+ }
154
+ normalAt(t) {
155
+ const { normals } = this;
156
+ if (normals.length === 1 || t <= 0) {
157
+ return normals[0];
158
+ }
159
+ if (t >= this.total) {
160
+ return normals[normals.length - 1];
161
+ }
162
+ const i = this.segment(t);
163
+ const { x, y } = lerp(normals[i - 1], normals[i], this.fraction(i, t));
164
+ const m = Math.hypot(x, y) || 1;
165
+ return { x: x / m, y: y / m };
166
+ }
167
+ };
168
+ function outwardNormals(pts, outRef) {
169
+ const centroid = centroidOf(pts);
170
+ return pts.map((p, i) => {
171
+ const t = unit(
172
+ pts[Math.max(0, i - 1)],
173
+ pts[Math.min(pts.length - 1, i + 1)]
174
+ );
175
+ const n = { x: -t.y, y: t.x };
176
+ const rx = p.x - centroid.x;
177
+ const ry = p.y - centroid.y;
178
+ const rm = Math.hypot(rx, ry);
179
+ const dot = n.x * rx + n.y * ry;
180
+ const ref = outRef[i];
181
+ const sign = rm > 1e-9 && Math.abs(dot) > 0.3 * rm ? Math.sign(dot) : Math.sign(n.x * ref.x + n.y * ref.y) || 1;
182
+ return { x: n.x * sign, y: n.y * sign };
183
+ });
184
+ }
185
+ function mapFlat(frame, tA, sign, flat) {
186
+ const ds = Math.min(5, Math.max(flat.length / 24, 0.5));
187
+ const pts = [];
188
+ const refs = [];
189
+ for (let t = 0; t <= flat.length + 1e-9; t += ds) {
190
+ const { u, v } = flat.at(Math.min(t, flat.length));
191
+ const q = frame.pointAt(tA + u);
192
+ const nr = frame.normalAt(tA + u);
193
+ const ox = nr.x * sign;
194
+ const oy = nr.y * sign;
195
+ pts.push({ x: q.x + ox * v, y: q.y + oy * v });
196
+ refs.push({ x: ox, y: oy });
197
+ }
198
+ return { pts, refs };
199
+ }
200
+ function fitted(frame, tA, sign, make, length) {
201
+ let flatLength = length;
202
+ let mapped = mapFlat(frame, tA, sign, make(flatLength));
203
+ if (frame.straight) {
204
+ return mapped;
205
+ }
206
+ for (let i = 0; i < 2; i++) {
207
+ const got = polylineLength(mapped.pts);
208
+ if (!(got > 0)) {
209
+ break;
210
+ }
211
+ flatLength *= length / got;
212
+ mapped = mapFlat(frame, tA, sign, make(flatLength));
213
+ }
214
+ return mapped;
215
+ }
216
+ function depths(from, n) {
217
+ return Array.from({ length: n }, (_, i) => from + i * LANE_GAP / 2);
218
+ }
219
+ function spanOf(anchors) {
220
+ if (anchors.length === 0) {
221
+ return Infinity;
222
+ }
223
+ let lo = Infinity;
224
+ let hi = -Infinity;
225
+ for (const a of anchors) {
226
+ lo = Math.min(lo, a.point.x);
227
+ hi = Math.max(hi, a.point.x);
228
+ }
229
+ return hi - lo;
230
+ }
231
+ function cellKey(gx, gy) {
232
+ return gx * 73856093 ^ gy * 19349663;
233
+ }
234
+ var Placer = class {
235
+ constructor(c) {
236
+ this.c = c;
237
+ this.grid = /* @__PURE__ */ new Map();
238
+ const { ids, first, count, per, seedX, seedY, backbone } = c;
239
+ const N = ids.length;
240
+ this.points = Array.from({ length: N }, () => void 0);
241
+ this.params = Array.from({ length: N }, () => void 0);
242
+ this.curves = Array.from({ length: N }, () => void 0);
243
+ this.links = c.links.map(
244
+ (sides, k) => sides.map((out) => out.filter(([v]) => v !== k))
245
+ );
246
+ this.route = ids.map((id) => id.startsWith(ROUTE_ID));
247
+ let min = Infinity;
248
+ let max = -Infinity;
249
+ for (let k = 0; k < N; k++) {
250
+ const start = first[k];
251
+ if (!backbone[start] || Number.isNaN(seedX[start])) {
252
+ continue;
253
+ }
254
+ min = Math.min(min, seedX[start]);
255
+ max = Math.max(max, seedX[start] + per[k] * (count[k] - 1));
256
+ }
257
+ this.backbone = new Curve(
258
+ [
259
+ { x: min, y: 0 },
260
+ { x: max, y: 0 }
261
+ ],
262
+ [
263
+ { x: 0, y: 1 },
264
+ { x: 0, y: 1 }
265
+ ],
266
+ true
267
+ );
268
+ this.backboneCentroid = { x: (min + max) / 2, y: 0 };
269
+ for (let k = 0; k < N; k++) {
270
+ const start = first[k];
271
+ if (!backbone[start] || Number.isNaN(seedX[start])) {
272
+ continue;
273
+ }
274
+ const pts = [];
275
+ const ts = [];
276
+ for (let i = 0; i < count[k]; i++) {
277
+ const x = seedX[start] + i * per[k];
278
+ pts.push({ x, y: seedY[start] });
279
+ ts.push(x - min);
280
+ }
281
+ this.points[k] = pts;
282
+ this.params[k] = ts;
283
+ this.curves[k] = this.backbone;
284
+ this.occupy(pts, this.backbone);
285
+ }
286
+ }
287
+ static hasBackbone(c) {
288
+ for (let k = 0; k < c.ids.length; k++) {
289
+ const start = c.first[k];
290
+ if (c.backbone[start] && !Number.isNaN(c.seedX[start])) {
291
+ return true;
292
+ }
293
+ }
294
+ return false;
295
+ }
296
+ run() {
297
+ const free = [];
298
+ for (let k = 0; k < this.points.length; k++) {
299
+ if (!this.points[k]) {
300
+ free.push(k);
301
+ }
302
+ }
303
+ const pieces = this.componentsOf(free).map((members) => ({ members, anchors: this.anchorsOf(members) })).filter((p) => p.anchors.length).map((p) => ({
304
+ members: p.members,
305
+ span: spanOf(p.anchors),
306
+ drawn: p.members.reduce((s, k) => s + this.drawn(k), 0)
307
+ })).sort((a, b) => a.span - b.span || a.drawn - b.drawn);
308
+ for (const p of pieces) {
309
+ this.placePiece(p.members, this.backboneCentroid);
310
+ }
311
+ }
312
+ write(X, Y) {
313
+ const { first } = this.c;
314
+ this.points.forEach((pts, k) => {
315
+ pts?.forEach((p, i) => {
316
+ X[first[k] + i] = p.x;
317
+ Y[first[k] + i] = p.y;
318
+ });
319
+ });
320
+ }
321
+ drawn(k) {
322
+ return this.c.per[k] * (this.c.count[k] - 1);
323
+ }
324
+ endIndex(k, side) {
325
+ return side ? this.c.count[k] - 1 : 0;
326
+ }
327
+ occupy(pts, curve) {
328
+ for (const p of pts) {
329
+ const key = cellKey(Math.floor(p.x / CLEAR), Math.floor(p.y / CLEAR));
330
+ const list = this.grid.get(key);
331
+ const occupant = { x: p.x, y: p.y, curve };
332
+ if (list) {
333
+ list.push(occupant);
334
+ } else {
335
+ this.grid.set(key, [occupant]);
336
+ }
337
+ }
338
+ }
339
+ // placed points within CLEAR of the candidate, off the exempt curve and away
340
+ // from the anchors, up to `limit`
341
+ violations(pts, exempt, anchorPts, limit) {
342
+ let n = 0;
343
+ for (const p of pts) {
344
+ if (anchorPts.some((a) => Math.hypot(p.x - a.x, p.y - a.y) < 2 * LANE_GAP)) {
345
+ continue;
346
+ }
347
+ const cx = Math.floor(p.x / CLEAR);
348
+ const cy = Math.floor(p.y / CLEAR);
349
+ for (let gx = cx - 1; gx <= cx + 1; gx++) {
350
+ for (let gy = cy - 1; gy <= cy + 1; gy++) {
351
+ for (const q of this.grid.get(cellKey(gx, gy)) ?? []) {
352
+ if (q.curve !== exempt && Math.hypot(p.x - q.x, p.y - q.y) < CLEAR) {
353
+ n++;
354
+ if (n >= limit) {
355
+ return n;
356
+ }
357
+ }
358
+ }
359
+ }
360
+ }
361
+ }
362
+ return n;
363
+ }
364
+ // the first candidate that clears what is drawn, else the least crowded
365
+ choose(candidates, exempt, anchorPts) {
366
+ let best;
367
+ let bestV = Infinity;
368
+ for (const candidate of candidates) {
369
+ const m = candidate();
370
+ const v = this.violations(m.pts, exempt, anchorPts, bestV);
371
+ if (v === 0) {
372
+ return m;
373
+ }
374
+ if (v < bestV) {
375
+ bestV = v;
376
+ best = m;
377
+ }
378
+ }
379
+ return best;
380
+ }
381
+ anchorsOf(members) {
382
+ const set = new Set(members);
383
+ const out = [];
384
+ for (const u of members) {
385
+ for (const uSide of SIDES) {
386
+ for (const [v, vSide] of this.links[u][uSide]) {
387
+ const pts = this.points[v];
388
+ if (set.has(v) || !pts) {
389
+ continue;
390
+ }
391
+ const i = this.endIndex(v, vSide);
392
+ out.push({
393
+ u,
394
+ uSide,
395
+ v,
396
+ vSide,
397
+ point: pts[i],
398
+ curve: this.curves[v],
399
+ t: this.params[v][i]
400
+ });
401
+ }
402
+ }
403
+ }
404
+ return out;
405
+ }
406
+ componentsOf(members) {
407
+ const set = new Set(members);
408
+ const seen = /* @__PURE__ */ new Set();
409
+ const comps = [];
410
+ for (const s of members) {
411
+ if (seen.has(s)) {
412
+ continue;
413
+ }
414
+ const queue = [s];
415
+ seen.add(s);
416
+ for (const u of queue) {
417
+ for (const side of SIDES) {
418
+ for (const [v] of this.links[u][side]) {
419
+ if (set.has(v) && !seen.has(v)) {
420
+ seen.add(v);
421
+ queue.push(v);
422
+ }
423
+ }
424
+ }
425
+ }
426
+ comps.push(queue);
427
+ }
428
+ return comps;
429
+ }
430
+ // side-consistent BFS over (node, entered side) within the piece; without a
431
+ // target, the path to the farthest state
432
+ spinePath(members, from, fromSide, to, toExitSide) {
433
+ const set = new Set(members);
434
+ const key = (k, s) => k * 2 + s;
435
+ const reached = (k, s) => k === to && other(s) === toExitSide;
436
+ const prev = /* @__PURE__ */ new Map([[key(from, fromSide), -1]]);
437
+ const queue = [[from, fromSide]];
438
+ let far = [from, fromSide];
439
+ for (const [k, s] of queue) {
440
+ far = [k, s];
441
+ if (to !== void 0 && reached(k, s)) {
442
+ break;
443
+ }
444
+ for (const [v, t] of this.links[k][other(s)]) {
445
+ if (!set.has(v) || prev.has(key(v, t))) {
446
+ continue;
447
+ }
448
+ prev.set(key(v, t), key(k, s));
449
+ queue.push([v, t]);
450
+ }
451
+ }
452
+ if (to !== void 0 && !reached(far[0], far[1])) {
453
+ return void 0;
454
+ }
455
+ const path = [];
456
+ for (let cur = key(far[0], far[1]); cur !== -1; cur = prev.get(cur)) {
457
+ path.push({ k: cur >> 1, enter: cur & 1 });
458
+ }
459
+ return path.reverse();
460
+ }
461
+ loosePath(members, from, to) {
462
+ const set = new Set(members);
463
+ const prev = /* @__PURE__ */ new Map([[from, -1]]);
464
+ const side = /* @__PURE__ */ new Map([[from, 0]]);
465
+ const queue = [from];
466
+ for (const k of queue) {
467
+ if (k === to) {
468
+ break;
469
+ }
470
+ for (const s of SIDES) {
471
+ for (const [v, t] of this.links[k][s]) {
472
+ if (!set.has(v) || prev.has(v)) {
473
+ continue;
474
+ }
475
+ prev.set(v, k);
476
+ side.set(v, t);
477
+ queue.push(v);
478
+ }
479
+ }
480
+ }
481
+ const path = [];
482
+ for (let cur = to; cur !== -1; cur = prev.get(cur)) {
483
+ path.push({ k: cur, enter: side.get(cur) });
484
+ }
485
+ return path.reverse();
486
+ }
487
+ spineLength(spine) {
488
+ const { link } = this.c;
489
+ return spine.reduce((s, { k }) => s + this.drawn(k) + link, link);
490
+ }
491
+ // the spine's chains along the curve from arc length t0, at their own
492
+ // spacing unless one is given
493
+ layAlong(spine, curve, t0, spacing) {
494
+ const { count, per, link } = this.c;
495
+ let t = t0;
496
+ const all = [];
497
+ for (const { k, enter } of spine) {
498
+ const step = spacing ?? per[k];
499
+ const pts = [];
500
+ const ts = [];
501
+ for (let i = 0; i < count[k]; i++) {
502
+ pts.push(curve.pointAt(t + i * step));
503
+ ts.push(t + i * step);
504
+ }
505
+ if (enter === 1) {
506
+ pts.reverse();
507
+ ts.reverse();
508
+ }
509
+ this.points[k] = pts;
510
+ this.params[k] = ts;
511
+ this.curves[k] = curve;
512
+ all.push(...pts);
513
+ t += step * (count[k] - 1) + link;
514
+ }
515
+ return all;
516
+ }
517
+ arcCandidates(frame, tA, sign, c, L) {
518
+ const hMax = (L - c) / 2 - 1e-6;
519
+ const tilts = L > 2 * c ? TEARDROP_TILTS : [0];
520
+ const candidates = [];
521
+ for (const drop of depths(0, 8)) {
522
+ for (const tilt of tilts) {
523
+ for (let i = 0; i <= 16; i++) {
524
+ const h = hMax * i / 16;
525
+ if (i < 16 && arcFlat(c, L, h, drop).apex < MIN_DEPTH) {
526
+ continue;
527
+ }
528
+ candidates.push(
529
+ () => fitted(
530
+ frame,
531
+ tA,
532
+ sign,
533
+ (length) => tilted(
534
+ arcFlat(
535
+ c,
536
+ length,
537
+ Math.min(h, (length - c) / 2 - 1e-6),
538
+ drop
539
+ ),
540
+ c,
541
+ tilt
542
+ ),
543
+ L
544
+ )
545
+ );
546
+ }
547
+ }
548
+ }
549
+ return candidates;
550
+ }
551
+ placePiece(members, parent) {
552
+ const { count, link } = this.c;
553
+ const anchors = this.anchorsOf(members);
554
+ if (anchors.length === 0) {
555
+ return;
556
+ }
557
+ const route = members.every((k) => this.route[k]);
558
+ const shared = anchors.every((a) => a.curve === anchors[0].curve) ? anchors[0].curve : void 0;
559
+ let frame;
560
+ let tA;
561
+ let tB;
562
+ let sign;
563
+ if (shared) {
564
+ anchors.sort((a, b) => a.t - b.t);
565
+ frame = shared;
566
+ tA = anchors[0].t;
567
+ tB = anchors[anchors.length - 1].t;
568
+ sign = frame === this.backbone && route ? -1 : 1;
569
+ } else {
570
+ anchors.sort((a2, b2) => a2.point.x - b2.point.x || a2.point.y - b2.point.y);
571
+ const a = anchors[0].point;
572
+ const b = anchors[anchors.length - 1].point;
573
+ const chord = Math.hypot(b.x - a.x, b.y - a.y);
574
+ const u = chord > 1e-9 ? unit(a, b) : { x: 1, y: 0 };
575
+ const n = { x: -u.y, y: u.x };
576
+ const mid = lerp(a, b, 0.5);
577
+ sign = anchors.some((x) => x.curve === this.backbone) ? (route ? -1 : 1) * (Math.sign(n.y) || 1) : (mid.x - parent.x) * n.x + (mid.y - parent.y) * n.y >= 0 ? 1 : -1;
578
+ frame = new Curve([a, b], [n, n], true);
579
+ tA = 0;
580
+ tB = chord;
581
+ }
582
+ const A = anchors[0];
583
+ const B = anchors[anchors.length - 1];
584
+ const c = tB - tA;
585
+ const single = anchors.length === 1 || A.u === B.u && A.uSide === B.uSide && c < 1e-9;
586
+ const anchorPts = [A.point, B.point];
587
+ const pick = (candidates) => this.choose(candidates, shared, anchorPts);
588
+ let spine;
589
+ let mapped;
590
+ let start = link;
591
+ let spacing;
592
+ if (single) {
593
+ spine = this.spinePath(members, A.u, A.uSide);
594
+ const L = this.spineLength(spine);
595
+ const next = A.vSide ? Math.max(0, count[A.v] - 2) : Math.min(1, count[A.v] - 1);
596
+ const dirU = A.t >= this.params[A.v][next] ? 1 : -1;
597
+ const du = dirU * Math.cos(TIP_ANGLE);
598
+ const dv = Math.sin(TIP_ANGLE);
599
+ mapped = pick(
600
+ depths(0, 12).map(
601
+ (drop) => () => fitted(
602
+ frame,
603
+ tA,
604
+ sign,
605
+ (length) => lineFlat(0, drop, du, dv, length),
606
+ L
607
+ )
608
+ )
609
+ );
610
+ } else {
611
+ spine = this.spinePath(members, A.u, A.uSide, B.u, B.uSide) ?? this.loosePath(members, A.u, B.u);
612
+ const L = this.spineLength(spine);
613
+ if (route) {
614
+ const d0 = Math.max(MIN_DEPTH, Math.min(ROUTE_BOW * c, ROUTE_CAP));
615
+ mapped = pick(
616
+ depths(d0, 12).map(
617
+ (d) => () => mapFlat(frame, tA, sign, arcFlat(c, archLength(c, d), 0, 0))
618
+ )
619
+ );
620
+ const total = polylineLength(mapped.pts);
621
+ spacing = Math.max(1e-3, (total - 2 * link) / (count[spine[0].k] - 1));
622
+ } else if (L > c + 1e-6) {
623
+ mapped = pick(this.arcCandidates(frame, tA, sign, c, L));
624
+ } else {
625
+ const bar = L - 2 * link;
626
+ mapped = pick(
627
+ depths(MIN_DEPTH, 12).map(
628
+ (d) => () => fitted(
629
+ frame,
630
+ tA,
631
+ sign,
632
+ (length) => lineFlat(c / 2 - length / 2, d, 1, 0, length),
633
+ bar
634
+ )
635
+ )
636
+ );
637
+ start = 0;
638
+ }
639
+ }
640
+ const curve = new Curve(mapped.pts, outwardNormals(mapped.pts, mapped.refs));
641
+ const placed = this.layAlong(spine, curve, start, spacing);
642
+ this.occupy(placed, curve);
643
+ const centroid = centroidOf(placed);
644
+ const rest = members.filter((k) => !this.points[k]);
645
+ const subs = this.componentsOf(rest).map((m) => ({ members: m, span: spanOf(this.anchorsOf(m)) })).sort((a, b) => a.span - b.span);
646
+ for (const sub of subs) {
647
+ this.placePiece(sub.members, centroid);
648
+ }
649
+ }
650
+ };
651
+ function stressPlacement(c) {
652
+ const X = new Float64Array(c.n).fill(NaN);
653
+ const Y = new Float64Array(c.n).fill(NaN);
654
+ if (Placer.hasBackbone(c)) {
655
+ const placer = new Placer(c);
656
+ placer.run();
657
+ placer.write(X, Y);
658
+ }
659
+ return { X, Y };
660
+ }
661
+
662
+ export {
663
+ stressPlacement
664
+ };