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.
- package/apk/dsh-remote.apk +0 -0
- package/gateway.cjs +261 -27
- package/package.json +1 -1
- package/public/announcements.json +25 -0
- package/public/app.js +570 -62
- package/public/desktop/desktop.css +42 -7
- package/public/desktop/desktop.html +30 -6
- package/public/desktop/desktop.js +395 -46
- package/public/index.html +57 -20
- package/public/md.js +71 -1
- package/public/morphicons-init.js +41 -0
- package/public/motion.js +469 -0
- package/public/plugin.html +4 -1
- package/public/plugin.js +1 -0
- package/public/styles.css +47 -6
- package/public/transcribe-core.js +74 -0
- package/public/update.json +12 -12
- package/public/vendor/gsap/NOTICE.md +10 -0
- package/public/vendor/gsap/gsap.min.js +11 -0
- package/public/vendor/morphicons/LICENSE +21 -0
- package/public/vendor/morphicons/README.md +10 -0
- package/public/vendor/morphicons/controller-CXZuwJ_M.js +152 -0
- package/public/vendor/morphicons/dom.js +206 -0
- package/public/vendor/morphicons/element.js +261 -0
- package/public/vendor/morphicons/normalize-CYnN3Npw.js +540 -0
- package/public/vendor/morphicons/spring-CFHloqPP.js +623 -0
- package/public/version.json +1 -1
|
@@ -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 };
|
package/public/version.json
CHANGED