dsh-remote-plugin 0.6.13 → 0.6.15

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,623 @@
1
+ import { n as iconToCubics } from "./normalize-CYnN3Npw.js";
2
+ //#region src/core/interpolate.ts
3
+ /** Preallocated output buffers for a plan (zero allocation per frame). */
4
+ function allocOutputs(plan) {
5
+ return plan.items.map(() => new Float64Array(2 * plan.n));
6
+ }
7
+ function interpPolar(plan, t, out) {
8
+ for (let k = 0; k < plan.items.length; k++) {
9
+ const it = plan.items[k];
10
+ const o = out[k];
11
+ const n = plan.n;
12
+ const s = Math.exp(it.lnSigma * t);
13
+ const ang = it.theta * t;
14
+ const cos = Math.cos(ang) * s;
15
+ const sin = Math.sin(ang) * s;
16
+ let cx;
17
+ let cy;
18
+ if (it.block) {
19
+ const [ox, oy] = it.block.off;
20
+ const [dx, dy] = it.block.drift;
21
+ cx = it.ca[0] + dx * t + (ox * cos - oy * sin - ox);
22
+ cy = it.ca[1] + dy * t + (ox * sin + oy * cos - oy);
23
+ } else {
24
+ cx = it.ca[0] + (it.cb[0] - it.ca[0]) * t;
25
+ cy = it.ca[1] + (it.cb[1] - it.ca[1]) * t;
26
+ }
27
+ for (let i = 0; i < n; i++) {
28
+ const px = it.aC[2 * i] + (it.bT[2 * i] - it.aC[2 * i]) * t;
29
+ const py = it.aC[2 * i + 1] + (it.bT[2 * i + 1] - it.aC[2 * i + 1]) * t;
30
+ o[2 * i] = cx + px * cos - py * sin;
31
+ o[2 * i + 1] = cy + px * sin + py * cos;
32
+ }
33
+ }
34
+ }
35
+ /** Raw coordinate lerp (same correspondence, no decomposition). */
36
+ function interpLinear(plan, t, out) {
37
+ for (let k = 0; k < plan.items.length; k++) {
38
+ const it = plan.items[k];
39
+ const o = out[k];
40
+ const n = plan.n;
41
+ for (let i = 0; i < n; i++) {
42
+ o[2 * i] = it.a[2 * i] + (it.bO[2 * i] - it.a[2 * i]) * t;
43
+ o[2 * i + 1] = it.a[2 * i + 1] + (it.bO[2 * i + 1] - it.a[2 * i + 1]) * t;
44
+ }
45
+ }
46
+ }
47
+ //#endregion
48
+ //#region src/core/plan.ts
49
+ /** Weight of |ΔL| in the subpath pairing cost. */
50
+ const LEN_WEIGHT = .35;
51
+ /** λ of the minimal-rotation tie-break: score = res + λ·|θ|/π.
52
+ * It exists because shapes symmetric under inversion (lines) tie in
53
+ * residual for both traversal orientations yet produce different rotations. */
54
+ const LAMBDA = .05;
55
+ /** Global residual below which the whole icon counts as congruent and the
56
+ * plan shares (θ, σ) across all items (hybrid variant of Procrustes). */
57
+ const GLOBAL_EPS = .005;
58
+ /** Bounds for exhaustive matching; above them it falls back to greedy with
59
+ * repair. 8! = 40 320 permutations / 1e5 assignments — both sub-ms. */
60
+ const PERM_MAX = 8;
61
+ const SURJ_MAX = 1e5;
62
+ function centroid(p) {
63
+ const n = p.length / 2;
64
+ let cx = 0;
65
+ let cy = 0;
66
+ for (let i = 0; i < n; i++) {
67
+ cx += p[2 * i];
68
+ cy += p[2 * i + 1];
69
+ }
70
+ return [cx / n, cy / n];
71
+ }
72
+ function polyLen(p) {
73
+ const n = p.length / 2;
74
+ let L = 0;
75
+ for (let i = 1; i < n; i++) L += Math.hypot(p[2 * i] - p[2 * i - 2], p[2 * i + 1] - p[2 * i - 1]);
76
+ return L;
77
+ }
78
+ function reversePts(p) {
79
+ const n = p.length / 2;
80
+ const out = new Float64Array(2 * n);
81
+ for (let i = 0; i < n; i++) {
82
+ out[2 * i] = p[2 * (n - 1 - i)];
83
+ out[2 * i + 1] = p[2 * (n - 1 - i) + 1];
84
+ }
85
+ return out;
86
+ }
87
+ /** Circular re-indexing of a loop: out[i] = p[(i+off) mod n]. Same point
88
+ * set, different cut point — the circular degree of freedom of closed paths. */
89
+ function rotatePts(p, off) {
90
+ const n = p.length / 2;
91
+ const out = new Float64Array(2 * n);
92
+ for (let i = 0; i < n; i++) {
93
+ const j = (i + off) % n;
94
+ out[2 * i] = p[2 * j];
95
+ out[2 * i + 1] = p[2 * j + 1];
96
+ }
97
+ return out;
98
+ }
99
+ /** Optimal similarity (θ, σ) minimizing Σ|σ·R(θ)·(a−c_A) − (b−c_B)|².
100
+ * θ* = atan2(S_xy − S_yx, S_xx + S_yy); σ* by zero derivative.
101
+ * res = RMS residual normalized by b's energy (0 → same shape). */
102
+ function procrustes(a, b, ca, cb) {
103
+ const n = a.length / 2;
104
+ let sxx = 0;
105
+ let sxy = 0;
106
+ let syx = 0;
107
+ let syy = 0;
108
+ let na = 0;
109
+ let nb = 0;
110
+ for (let i = 0; i < n; i++) {
111
+ const ax = a[2 * i] - ca[0];
112
+ const ay = a[2 * i + 1] - ca[1];
113
+ const bx = b[2 * i] - cb[0];
114
+ const by = b[2 * i + 1] - cb[1];
115
+ sxx += ax * bx;
116
+ syy += ay * by;
117
+ sxy += ax * by;
118
+ syx += ay * bx;
119
+ na += ax * ax + ay * ay;
120
+ nb += bx * bx + by * by;
121
+ }
122
+ const theta = Math.atan2(sxy - syx, sxx + syy);
123
+ const num = Math.cos(theta) * (sxx + syy) + Math.sin(theta) * (sxy - syx);
124
+ let sigma = na > 1e-12 ? num / na : 1;
125
+ if (!(sigma > 1e-6)) sigma = 1e-6;
126
+ const res2 = Math.max(0, sigma * sigma * na - 2 * sigma * num + nb);
127
+ const res = nb > 1e-12 ? Math.sqrt(res2 / nb) : 0;
128
+ return {
129
+ theta,
130
+ sigma,
131
+ res
132
+ };
133
+ }
134
+ /** Best index-to-index correspondence between a and b: tries both traversal
135
+ * directions and, if there is a closed loop, its N circular offsets,
136
+ * scoring with score = res + λ·|θ|/π. The freedom is applied to ONE cloud
137
+ * — the closed one (b if both are); varying both at once would be
138
+ * redundant. */
139
+ function alignPair(aPts, bPts, aClosed = false, bClosed = false) {
140
+ const ca = centroid(aPts);
141
+ const cb = centroid(bPts);
142
+ const varyA = aClosed && !bClosed;
143
+ const base = varyA ? aPts : bPts;
144
+ const offs = aClosed || bClosed ? base.length / 2 : 1;
145
+ let bestScore = Number.POSITIVE_INFINITY;
146
+ let best = base;
147
+ let sim = {
148
+ theta: 0,
149
+ sigma: 1,
150
+ res: 0
151
+ };
152
+ for (let dir = 0; dir < 2; dir++) {
153
+ const walk = dir ? reversePts(base) : base;
154
+ for (let off = 0; off < offs; off++) {
155
+ const cand = off ? rotatePts(walk, off) : walk;
156
+ const s = varyA ? procrustes(cand, bPts, ca, cb) : procrustes(aPts, cand, ca, cb);
157
+ const score = s.res + LAMBDA * Math.abs(s.theta) / Math.PI;
158
+ if (score < bestScore) {
159
+ bestScore = score;
160
+ best = cand;
161
+ sim = s;
162
+ }
163
+ }
164
+ }
165
+ return varyA ? {
166
+ ca,
167
+ cb,
168
+ a: best,
169
+ b: bPts,
170
+ ...sim
171
+ } : {
172
+ ca,
173
+ cb,
174
+ a: aPts,
175
+ b: best,
176
+ ...sim
177
+ };
178
+ }
179
+ function costMatrix(A, B) {
180
+ const cbs = B.map(centroid);
181
+ const lbs = B.map(polyLen);
182
+ return A.map((a) => {
183
+ const ca = centroid(a);
184
+ const la = polyLen(a);
185
+ return cbs.map((cb, j) => Math.hypot(ca[0] - cb[0], ca[1] - cb[1]) + LEN_WEIGHT * Math.abs(la - lbs[j]));
186
+ });
187
+ }
188
+ function bestPermutation(C) {
189
+ const n = C.length;
190
+ if (n > PERM_MAX) {
191
+ const pairs = [];
192
+ for (let i = 0; i < n; i++) for (let j = 0; j < n; j++) pairs.push([
193
+ C[i][j],
194
+ i,
195
+ j
196
+ ]);
197
+ pairs.sort((x, y) => x[0] - y[0]);
198
+ const out = new Array(n).fill(-1);
199
+ const used = new Array(n).fill(false);
200
+ for (const [, i, j] of pairs) if (out[i] < 0 && !used[j]) {
201
+ out[i] = j;
202
+ used[j] = true;
203
+ }
204
+ return out;
205
+ }
206
+ const idx = Array.from({ length: n }, (_, i) => i);
207
+ let best = idx.slice();
208
+ let bc = Number.POSITIVE_INFINITY;
209
+ const perm = (arr, k, acc) => {
210
+ if (acc >= bc) return;
211
+ if (k === n) {
212
+ bc = acc;
213
+ best = arr.slice();
214
+ return;
215
+ }
216
+ for (let i = k; i < n; i++) {
217
+ [arr[k], arr[i]] = [arr[i], arr[k]];
218
+ perm(arr, k + 1, acc + C[k][arr[k]]);
219
+ [arr[k], arr[i]] = [arr[i], arr[k]];
220
+ }
221
+ };
222
+ perm(idx, 0, 0);
223
+ return best;
224
+ }
225
+ function bestSurjection(C) {
226
+ const B = C.length;
227
+ const S = C[0].length;
228
+ if (S ** B > SURJ_MAX) {
229
+ const f = C.map((row) => {
230
+ let m = 0;
231
+ for (let j = 1; j < row.length; j++) if (row[j] < row[m]) m = j;
232
+ return m;
233
+ });
234
+ const mult = new Array(S).fill(0);
235
+ for (const s of f) mult[s]++;
236
+ for (let s = 0; s < S; s++) {
237
+ if (mult[s] > 0) continue;
238
+ let bi = -1;
239
+ let bc = Number.POSITIVE_INFINITY;
240
+ for (let i = 0; i < B; i++) {
241
+ if (mult[f[i]] < 2) continue;
242
+ const extra = C[i][s] - C[i][f[i]];
243
+ if (extra < bc) {
244
+ bc = extra;
245
+ bi = i;
246
+ }
247
+ }
248
+ mult[f[bi]]--;
249
+ f[bi] = s;
250
+ mult[s]++;
251
+ }
252
+ return f;
253
+ }
254
+ let best = null;
255
+ let bc = Number.POSITIVE_INFINITY;
256
+ const f = new Array(B);
257
+ const mult = new Array(S).fill(0);
258
+ const rec = (i, acc, covered) => {
259
+ if (acc >= bc || S - covered > B - i) return;
260
+ if (i === B) {
261
+ bc = acc;
262
+ best = f.slice();
263
+ return;
264
+ }
265
+ for (let s = 0; s < S; s++) {
266
+ f[i] = s;
267
+ mult[s]++;
268
+ rec(i + 1, acc + C[i][s], covered + (mult[s] === 1 ? 1 : 0));
269
+ mult[s]--;
270
+ }
271
+ };
272
+ rec(0, 0, 0);
273
+ if (!best) throw new Error("morphicons: no valid surjection (B < S)");
274
+ return best;
275
+ }
276
+ function applyGlobal(items, n) {
277
+ const T = items.length * n;
278
+ const ga = new Float64Array(2 * T);
279
+ const gb = new Float64Array(2 * T);
280
+ items.forEach((it, k) => {
281
+ ga.set(it.a, 2 * n * k);
282
+ gb.set(it.bO, 2 * n * k);
283
+ });
284
+ const gca = centroid(ga);
285
+ const g = procrustes(ga, gb, gca, centroid(gb));
286
+ if (g.res >= GLOBAL_EPS) return;
287
+ const cos = Math.cos(-g.theta);
288
+ const sin = Math.sin(-g.theta);
289
+ const rc = Math.cos(g.theta);
290
+ const rs = Math.sin(g.theta);
291
+ for (const it of items) {
292
+ let e2 = 0;
293
+ let nb = 0;
294
+ for (let i = 0; i < n; i++) {
295
+ const bx = it.bO[2 * i] - it.cb[0];
296
+ const by = it.bO[2 * i + 1] - it.cb[1];
297
+ it.bT[2 * i] = (bx * cos - by * sin) / g.sigma;
298
+ it.bT[2 * i + 1] = (bx * sin + by * cos) / g.sigma;
299
+ const ex = g.sigma * (rc * it.aC[2 * i] - rs * it.aC[2 * i + 1]) - bx;
300
+ const ey = g.sigma * (rs * it.aC[2 * i] + rc * it.aC[2 * i + 1]) - by;
301
+ e2 += ex * ex + ey * ey;
302
+ nb += bx * bx + by * by;
303
+ }
304
+ it.theta = g.theta;
305
+ it.lnSigma = Math.log(g.sigma);
306
+ it.res = nb > 1e-12 ? Math.sqrt(e2 / nb) : 0;
307
+ const s1 = Math.exp(it.lnSigma);
308
+ const c1 = Math.cos(it.theta) * s1;
309
+ const n1 = Math.sin(it.theta) * s1;
310
+ const ox = it.ca[0] - gca[0];
311
+ const oy = it.ca[1] - gca[1];
312
+ const rx = ox * c1 - oy * n1 - ox;
313
+ const ry = ox * n1 + oy * c1 - oy;
314
+ it.block = {
315
+ off: [ox, oy],
316
+ drift: [it.cb[0] - it.ca[0] - rx, it.cb[1] - it.ca[1] - ry]
317
+ };
318
+ }
319
+ }
320
+ /** Builds the morph plan between two lists of sampled subpaths. The plan is
321
+ * cacheable and serializable; it accepts any list — including intermediate
322
+ * shapes (interruptions). */
323
+ function buildPlan(srcSubs, dstSubs) {
324
+ const p = srcSubs.length;
325
+ const q = dstSubs.length;
326
+ if (p === 0 || q === 0) throw new Error("morphicons: icon has no subpaths");
327
+ const A = srcSubs.map((s) => s.pts);
328
+ const B = dstSubs.map((s) => s.pts);
329
+ const pairs = [];
330
+ if (p === q) {
331
+ const perm = bestPermutation(costMatrix(A, B));
332
+ for (let i = 0; i < p; i++) pairs.push([i, perm[i]]);
333
+ } else if (p < q) {
334
+ const f = bestSurjection(costMatrix(B, A));
335
+ for (let j = 0; j < q; j++) pairs.push([f[j], j]);
336
+ } else {
337
+ const f = bestSurjection(costMatrix(A, B));
338
+ for (let i = 0; i < p; i++) pairs.push([i, f[i]]);
339
+ }
340
+ const n = A[0].length / 2;
341
+ const items = pairs.map(([si, di]) => {
342
+ const al = alignPair(A[si], B[di], srcSubs[si].closed, dstSubs[di].closed);
343
+ const a = al.a;
344
+ const aC = new Float64Array(2 * n);
345
+ const bT = new Float64Array(2 * n);
346
+ const bO = new Float64Array(2 * n);
347
+ const cos = Math.cos(-al.theta);
348
+ const sin = Math.sin(-al.theta);
349
+ for (let i = 0; i < n; i++) {
350
+ aC[2 * i] = a[2 * i] - al.ca[0];
351
+ aC[2 * i + 1] = a[2 * i + 1] - al.ca[1];
352
+ const bx = al.b[2 * i] - al.cb[0];
353
+ const by = al.b[2 * i + 1] - al.cb[1];
354
+ bT[2 * i] = (bx * cos - by * sin) / al.sigma;
355
+ bT[2 * i + 1] = (bx * sin + by * cos) / al.sigma;
356
+ bO[2 * i] = al.b[2 * i];
357
+ bO[2 * i + 1] = al.b[2 * i + 1];
358
+ }
359
+ return {
360
+ a,
361
+ aC,
362
+ bT,
363
+ bO,
364
+ ca: al.ca,
365
+ cb: al.cb,
366
+ theta: al.theta,
367
+ lnSigma: Math.log(al.sigma),
368
+ res: al.res,
369
+ closed: srcSubs[si].closed && dstSubs[di].closed,
370
+ block: null
371
+ };
372
+ });
373
+ if (items.length > 1) applyGlobal(items, n);
374
+ return {
375
+ items,
376
+ n
377
+ };
378
+ }
379
+ //#endregion
380
+ //#region src/core/resample.ts
381
+ /** Default angular threshold for a segment joint to count as a corner. */
382
+ const CORNER_THRESHOLD = Math.PI / 8;
383
+ const GX = [
384
+ .18343464249564978,
385
+ .525532409916329,
386
+ .7966664774136267,
387
+ .9602898564975363
388
+ ];
389
+ const GW = [
390
+ .362683783378362,
391
+ .31370664587788727,
392
+ .22238103445337448,
393
+ .10122853629037626
394
+ ];
395
+ function speed(p, k, t) {
396
+ const i = 6 * k;
397
+ const u = 1 - t;
398
+ const c0 = 3 * u * u;
399
+ const c1 = 6 * u * t;
400
+ const c2 = 3 * t * t;
401
+ const dx = c0 * (p[i + 2] - p[i]) + c1 * (p[i + 4] - p[i + 2]) + c2 * (p[i + 6] - p[i + 4]);
402
+ const dy = c0 * (p[i + 3] - p[i + 1]) + c1 * (p[i + 5] - p[i + 3]) + c2 * (p[i + 7] - p[i + 5]);
403
+ return Math.hypot(dx, dy);
404
+ }
405
+ function segLen(p, k, t1 = 1) {
406
+ const half = t1 / 2;
407
+ let s = 0;
408
+ for (let j = 0; j < 4; j++) s += GW[j] * (speed(p, k, half + half * GX[j]) + speed(p, k, half - half * GX[j]));
409
+ return s * half;
410
+ }
411
+ function point(p, k, t, out, o) {
412
+ const i = 6 * k;
413
+ const u = 1 - t;
414
+ const b0 = u * u * u;
415
+ const b1 = 3 * u * u * t;
416
+ const b2 = 3 * u * t * t;
417
+ const b3 = t * t * t;
418
+ out[o] = b0 * p[i] + b1 * p[i + 2] + b2 * p[i + 4] + b3 * p[i + 6];
419
+ out[o + 1] = b0 * p[i + 1] + b1 * p[i + 3] + b2 * p[i + 5] + b3 * p[i + 7];
420
+ }
421
+ function tangent(p, k, atEnd) {
422
+ const i = 6 * k;
423
+ const b = atEnd ? i + 6 : i;
424
+ const s = atEnd ? -1 : 1;
425
+ for (const j of atEnd ? [
426
+ 4,
427
+ 2,
428
+ 0
429
+ ] : [
430
+ 2,
431
+ 4,
432
+ 6
433
+ ]) {
434
+ const dx = s * (p[i + j] - p[b]);
435
+ const dy = s * (p[i + j + 1] - p[b + 1]);
436
+ if (dx * dx + dy * dy > 1e-18) return [dx, dy];
437
+ }
438
+ return null;
439
+ }
440
+ /** Segment boundaries (index of the segment starting at the corner) whose
441
+ * tangent discontinuity exceeds the threshold. For closed paths this
442
+ * includes the closing joint (boundary = first active segment). */
443
+ function detectCorners(path, threshold = CORNER_THRESHOLD) {
444
+ const p = path.pts;
445
+ const m = (p.length / 2 - 1) / 3;
446
+ const active = [];
447
+ for (let k = 0; k < m; k++) if (segLen(p, k) > 1e-9) active.push(k);
448
+ if (active.length === 0) return [];
449
+ const corners = /* @__PURE__ */ new Set();
450
+ const test = (a, b) => {
451
+ const u = tangent(p, a, true);
452
+ const v = tangent(p, b, false);
453
+ if (!u || !v) return;
454
+ if (Math.abs(Math.atan2(u[0] * v[1] - u[1] * v[0], u[0] * v[0] + u[1] * v[1])) > threshold) corners.add(b);
455
+ };
456
+ for (let j = 0; j + 1 < active.length; j++) test(active[j], active[j + 1]);
457
+ if (path.closed && active.length > 1) test(active[active.length - 1], active[0]);
458
+ return [...corners].sort((a, b) => a - b);
459
+ }
460
+ function invert(p, k, s, ls) {
461
+ if (s <= 0) return 0;
462
+ if (s >= ls) return 1;
463
+ let lo = 0;
464
+ let hi = 1;
465
+ let t = s / ls;
466
+ for (let it = 0; it < 12; it++) {
467
+ const f = segLen(p, k, t) - s;
468
+ if (Math.abs(f) < 1e-10 * ls + 1e-14) break;
469
+ if (f > 0) hi = t;
470
+ else lo = t;
471
+ const sp = speed(p, k, t);
472
+ let nt = sp > 1e-12 ? t - f / sp : (lo + hi) / 2;
473
+ if (!(nt > lo && nt < hi)) nt = (lo + hi) / 2;
474
+ t = nt;
475
+ }
476
+ return t;
477
+ }
478
+ /** Samples a cubic subpath at N points equidistant by arc length, anchoring
479
+ * corners and endpoints as exact samples. Returns Float64Array(2N). Closed
480
+ * paths distribute N intervals around the loop (without duplicating the
481
+ * first point); the circular start-point freedom is resolved by the plan's
482
+ * circular correspondence. */
483
+ function resamplePath(path, N = 64, cornerThreshold = CORNER_THRESHOLD) {
484
+ const p = path.pts;
485
+ const m = (p.length / 2 - 1) / 3;
486
+ const out = new Float64Array(2 * N);
487
+ const fill = () => {
488
+ for (let i = 0; i < N; i++) {
489
+ out[2 * i] = p[0];
490
+ out[2 * i + 1] = p[1];
491
+ }
492
+ return out;
493
+ };
494
+ if (m < 1) return fill();
495
+ const lens = new Array(m);
496
+ let L = 0;
497
+ for (let k = 0; k < m; k++) {
498
+ lens[k] = segLen(p, k);
499
+ L += lens[k];
500
+ }
501
+ if (L < 1e-12) return fill();
502
+ const cs = detectCorners(path, cornerThreshold);
503
+ const anchors = path.closed ? cs.length > 0 ? cs : [0] : [.../* @__PURE__ */ new Set([
504
+ 0,
505
+ ...cs,
506
+ m
507
+ ])].sort((a, b) => a - b);
508
+ const runs = [];
509
+ if (path.closed) for (let j = 0; j < anchors.length; j++) {
510
+ const a = anchors[j];
511
+ const b = j + 1 < anchors.length ? anchors[j + 1] : anchors[0] + m;
512
+ runs.push([a, b]);
513
+ }
514
+ else for (let j = 0; j + 1 < anchors.length; j++) runs.push([anchors[j], anchors[j + 1]]);
515
+ const rl = runs.map(([a, b]) => {
516
+ let s = 0;
517
+ for (let k = a; k < b; k++) s += lens[k % m];
518
+ return s;
519
+ });
520
+ const intervals = path.closed ? N : N - 1;
521
+ if (runs.length > intervals) throw new Error(`morphicons: N=${N} too small (${runs.length} runs)`);
522
+ const total = rl.reduce((a, b) => a + b, 0) || 1;
523
+ const ideal = rl.map((l) => intervals * l / total);
524
+ const counts = ideal.map((q) => Math.max(1, Math.floor(q)));
525
+ let R = intervals - counts.reduce((a, b) => a + b, 0);
526
+ if (R > 0) {
527
+ const order = ideal.map((q, idx) => [Math.round((q - Math.floor(q)) * 1e9), idx]).sort((a, b) => b[0] - a[0] || a[1] - b[1]);
528
+ for (let j = 0; j < R; j++) counts[order[j % counts.length][1]]++;
529
+ }
530
+ while (R < 0) {
531
+ let bi = 0;
532
+ for (let idx = 1; idx < counts.length; idx++) if (counts[idx] > counts[bi]) bi = idx;
533
+ if (counts[bi] <= 1) break;
534
+ counts[bi]--;
535
+ R++;
536
+ }
537
+ let w = 0;
538
+ for (let r = 0; r < runs.length; r++) {
539
+ const [k0, k1] = runs[r];
540
+ const cnt = counts[r];
541
+ const Lr = rl[r];
542
+ const vi = 6 * (k0 % m);
543
+ out[2 * w] = p[vi];
544
+ out[2 * w + 1] = p[vi + 1];
545
+ w++;
546
+ let seg = k0;
547
+ let acc = 0;
548
+ for (let j = 1; j < cnt; j++) {
549
+ const target = Lr * j / cnt;
550
+ while (seg < k1 - 1 && acc + lens[seg % m] < target) {
551
+ acc += lens[seg % m];
552
+ seg++;
553
+ }
554
+ const k = seg % m;
555
+ const ls = lens[k];
556
+ point(p, k, ls > 1e-12 ? invert(p, k, target - acc, ls) : 0, out, 2 * w);
557
+ w++;
558
+ }
559
+ }
560
+ if (!path.closed) {
561
+ const vi = 6 * m;
562
+ out[2 * w] = p[vi];
563
+ out[2 * w + 1] = p[vi + 1];
564
+ }
565
+ return out;
566
+ }
567
+ /** Full input pipeline: icon → cubics → sampled subpaths with their
568
+ * topology (the plan needs to know which subpaths are closed loops). */
569
+ function resampleIcon(input, N = 64) {
570
+ return iconToCubics(input).map((path) => ({
571
+ pts: resamplePath(path, N),
572
+ closed: path.closed
573
+ }));
574
+ }
575
+ //#endregion
576
+ //#region src/core/spring.ts
577
+ var Spring = class {
578
+ x = 1;
579
+ v = 0;
580
+ k = 250;
581
+ c = 24;
582
+ config(k, c) {
583
+ this.k = k;
584
+ this.c = c;
585
+ }
586
+ /** Starts (or restarts mid-flight) preserving velocity. */
587
+ start() {
588
+ this.x = 0;
589
+ if (this.v > 14) this.v = 14;
590
+ if (this.v < -14) this.v = -14;
591
+ }
592
+ /** Advances dt seconds. Returns true on settle (|1−x| < 0.001 ∧ |v| < 0.02). */
593
+ step(dt) {
594
+ const steps = Math.max(1, Math.min(16, Math.ceil(dt / (1 / 240))));
595
+ const s = dt / steps;
596
+ for (let i = 0; i < steps; i++) {
597
+ const a = this.k * (1 - this.x) - this.c * this.v;
598
+ this.v += a * s;
599
+ this.x += this.v * s;
600
+ }
601
+ return Math.abs(1 - this.x) < .001 && Math.abs(this.v) < .02;
602
+ }
603
+ };
604
+ /** Spring presets (ζ = c/(2√k)) with the API's public names. */
605
+ const SPRING_PRESETS = {
606
+ /** ζ = 1.00 — critically damped, no overshoot. */
607
+ smooth: {
608
+ k: 170,
609
+ c: 26
610
+ },
611
+ /** ζ = 0.73 — fast, subtle overshoot. */
612
+ snappy: {
613
+ k: 420,
614
+ c: 30
615
+ },
616
+ /** ζ = 0.40 — playful. */
617
+ bouncy: {
618
+ k: 300,
619
+ c: 14
620
+ }
621
+ };
622
+ //#endregion
623
+ export { buildPlan as a, interpPolar as c, resamplePath as i, Spring as n, allocOutputs as o, resampleIcon as r, interpLinear as s, SPRING_PRESETS as t };
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "0.6.13"
2
+ "version": "0.6.15"
3
3
  }