@kgdiff/scene 0.0.0-stage → 0.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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +50 -2
  3. package/dist/build.d.ts +23 -0
  4. package/dist/build.d.ts.map +1 -0
  5. package/dist/build.js +407 -0
  6. package/dist/build.js.map +1 -0
  7. package/dist/choreography.d.ts +98 -0
  8. package/dist/choreography.d.ts.map +1 -0
  9. package/dist/choreography.js +271 -0
  10. package/dist/choreography.js.map +1 -0
  11. package/dist/color.d.ts +42 -0
  12. package/dist/color.d.ts.map +1 -0
  13. package/dist/color.js +169 -0
  14. package/dist/color.js.map +1 -0
  15. package/dist/constants.d.ts +89 -0
  16. package/dist/constants.d.ts.map +1 -0
  17. package/dist/constants.js +89 -0
  18. package/dist/constants.js.map +1 -0
  19. package/dist/easing.d.ts +28 -0
  20. package/dist/easing.d.ts.map +1 -0
  21. package/dist/easing.js +103 -0
  22. package/dist/easing.js.map +1 -0
  23. package/dist/focus.d.ts +45 -0
  24. package/dist/focus.d.ts.map +1 -0
  25. package/dist/focus.js +317 -0
  26. package/dist/focus.js.map +1 -0
  27. package/dist/frame.d.ts +13 -0
  28. package/dist/frame.d.ts.map +1 -0
  29. package/dist/frame.js +32 -0
  30. package/dist/frame.js.map +1 -0
  31. package/dist/geometry.d.ts +61 -0
  32. package/dist/geometry.d.ts.map +1 -0
  33. package/dist/geometry.js +122 -0
  34. package/dist/geometry.js.map +1 -0
  35. package/dist/index.d.ts +16 -0
  36. package/dist/index.d.ts.map +1 -0
  37. package/dist/index.js +16 -0
  38. package/dist/index.js.map +1 -0
  39. package/dist/resolver.d.ts +9 -0
  40. package/dist/resolver.d.ts.map +1 -0
  41. package/dist/resolver.js +1621 -0
  42. package/dist/resolver.js.map +1 -0
  43. package/dist/roles.d.ts +46 -0
  44. package/dist/roles.d.ts.map +1 -0
  45. package/dist/roles.js +197 -0
  46. package/dist/roles.js.map +1 -0
  47. package/dist/styles.d.ts +116 -0
  48. package/dist/styles.d.ts.map +1 -0
  49. package/dist/styles.js +391 -0
  50. package/dist/styles.js.map +1 -0
  51. package/dist/theme.d.ts +14 -0
  52. package/dist/theme.d.ts.map +1 -0
  53. package/dist/theme.js +154 -0
  54. package/dist/theme.js.map +1 -0
  55. package/dist/timeline.d.ts +69 -0
  56. package/dist/timeline.d.ts.map +1 -0
  57. package/dist/timeline.js +362 -0
  58. package/dist/timeline.js.map +1 -0
  59. package/dist/types.d.ts +493 -0
  60. package/dist/types.d.ts.map +1 -0
  61. package/dist/types.js +2 -0
  62. package/dist/types.js.map +1 -0
  63. package/dist/view.d.ts +11 -0
  64. package/dist/view.d.ts.map +1 -0
  65. package/dist/view.js +22 -0
  66. package/dist/view.js.map +1 -0
  67. package/package.json +46 -3
  68. package/src/build.ts +450 -0
  69. package/src/choreography.ts +342 -0
  70. package/src/color.ts +194 -0
  71. package/src/constants.ts +97 -0
  72. package/src/easing.ts +104 -0
  73. package/src/focus.ts +335 -0
  74. package/src/frame.ts +37 -0
  75. package/src/geometry.ts +162 -0
  76. package/src/index.ts +15 -0
  77. package/src/resolver.ts +1710 -0
  78. package/src/roles.ts +203 -0
  79. package/src/styles.ts +513 -0
  80. package/src/theme.ts +185 -0
  81. package/src/timeline.ts +391 -0
  82. package/src/types.ts +544 -0
  83. package/src/view.ts +27 -0
@@ -0,0 +1,1621 @@
1
+ /**
2
+ * `createVisualResolver`: ViewState → FrameVisuals, the per-frame heart of the visual language.
3
+ *
4
+ * Construction precomputes everything static (per-layer styles, label strings, color ramps,
5
+ * the replay choreography, glyph specs). `resolve(view, out)` then copies styles into the
6
+ * reused `out` objects, animates only the elements inside active replay windows and applies
7
+ * the view modulators (focus, family filter, spotlight, hover, selection, LOD).
8
+ *
9
+ * Allocation guarantee: with `out`, steady-state frames allocate no objects and no arrays —
10
+ * element objects, nested ring / halo / label / badge objects and glyph objects are pooled per
11
+ * frame object, and every per-element number is read from flat typed-array snapshots. What
12
+ * remains is transient V8 HeapNumber boxing of doubles in code the JIT has not inlined or
13
+ * optimized yet: none in static frames (union / before / after, hover, selection, filters), a
14
+ * few boxes per *animating* element in replay (measured ≲ 1–10 KB per frame for thousands of
15
+ * nodes, depending on JIT tier and module format). That is short-lived young-generation garbage,
16
+ * collected by scavenges in well under a millisecond, and is tolerated by design.
17
+ *
18
+ * Alpha convention: `NodeVisual.alpha` / `EdgeVisual.alpha` / `GlyphVisual.alpha` apply to the
19
+ * mark; nested alphas (ring, halo, labels; badges have none) are relative to it.
20
+ */
21
+ import { KgdiffError } from '@kgdiff/core';
22
+ import { CHOREO, choreograph, EA_FADE, EA_FOLLOW_MERGE, EA_FOLLOW_SPLIT, EA_GROW, EA_NONE, EA_RETRACT, EA_SWAP, EA_SWING, NA_ADD, NA_EMERGE, NA_FADE, NA_NONE, NA_REMOVE, NA_TRAVEL, } from './choreography.js';
23
+ import { rampAt } from './color.js';
24
+ import { DASH_DOTTED, DASH_EDGE, DASH_GHOST, EMPHASIS, LABEL, LOD, PHASE, STROKE, } from './constants.js';
25
+ import { backOut, bump, clamp01, easeInOut, easeStandard, lerp, segment } from './easing.js';
26
+ import { edgeCurvatures } from './geometry.js';
27
+ import { EL_BEFORE, EL_NONE, EL_UNION, edgeLayerStyles, F_IDENTITY, F_NEUTRAL, F_REMOVED, F_STRUCTURE, FAMILY_BY_CODE, familyCode, nodeLayerStyles, StyleBook, tokensFor, } from './styles.js';
28
+ const L_UNION = 0;
29
+ const L_BEFORE = 1;
30
+ const L_AFTER = 2;
31
+ const L_REPLAY = 3;
32
+ const SPOT_END = PHASE.spotlightEnd;
33
+ const SETTLE = PHASE.settleStart;
34
+ const TRANSFORM = SETTLE - SPOT_END;
35
+ const CHANGE_FAMILIES = [
36
+ 'added',
37
+ 'removed',
38
+ 'content',
39
+ 'structure',
40
+ 'identity',
41
+ ];
42
+ const NO_GROUPS = Object.freeze([]);
43
+ /** Glyph roles (what a glyph spec is for). */
44
+ const G_TRAIL = 0;
45
+ const G_REPLACEMENT = 1;
46
+ const G_GHOST = 2;
47
+ const G_PIVOT = 3;
48
+ const G_SUGGESTION = 4;
49
+ const newLabel = () => ({
50
+ text: '',
51
+ color: '',
52
+ alpha: 0,
53
+ size: 0,
54
+ weight: 400,
55
+ strike: false,
56
+ priority: 0,
57
+ minZoom: 0,
58
+ offset: 0,
59
+ });
60
+ const newNode = () => ({
61
+ visible: false,
62
+ x: 0,
63
+ y: 0,
64
+ r: 0,
65
+ alpha: 0,
66
+ family: 'neutral',
67
+ fill: null,
68
+ stroke: null,
69
+ strokeWidth: 0,
70
+ dash: null,
71
+ ring: null,
72
+ halo: null,
73
+ label: null,
74
+ secondaryLabel: null,
75
+ badge: null,
76
+ pulse: 0,
77
+ z: 0,
78
+ });
79
+ const newEdge = () => ({
80
+ visible: false,
81
+ x1: 0,
82
+ y1: 0,
83
+ x2: 0,
84
+ y2: 0,
85
+ r1: 0,
86
+ r2: 0,
87
+ curvature: 0,
88
+ color: '',
89
+ width: 0,
90
+ alpha: 0,
91
+ dash: null,
92
+ dashOffset: 0,
93
+ arrow: false,
94
+ arrowMinZoom: 0,
95
+ progress: 1,
96
+ fromTarget: false,
97
+ label: null,
98
+ z: 0,
99
+ });
100
+ const newGlyph = () => ({
101
+ kind: 'pivot',
102
+ x1: 0,
103
+ y1: 0,
104
+ x2: 0,
105
+ y2: 0,
106
+ r1: 0,
107
+ r2: 0,
108
+ color: '',
109
+ alpha: 0,
110
+ width: 0,
111
+ dash: null,
112
+ progress: 1,
113
+ curvature: 0,
114
+ label: null,
115
+ });
116
+ function hideNode(v) {
117
+ v.visible = false;
118
+ v.alpha = 0;
119
+ v.ring = null;
120
+ v.halo = null;
121
+ v.label = null;
122
+ v.secondaryLabel = null;
123
+ v.badge = null;
124
+ v.pulse = 0;
125
+ }
126
+ function hideEdge(v) {
127
+ v.visible = false;
128
+ v.alpha = 0;
129
+ v.label = null;
130
+ }
131
+ class SceneResolver {
132
+ scene;
133
+ t;
134
+ N;
135
+ M;
136
+ H;
137
+ // Static per-element data.
138
+ nStyles;
139
+ eStyles;
140
+ nFam;
141
+ nHunk;
142
+ nHunkFam;
143
+ eFam;
144
+ eHunk;
145
+ eHunkFam;
146
+ eUnionText;
147
+ ghostCurv;
148
+ fillRamp;
149
+ strokeRamp;
150
+ ringRamp;
151
+ c;
152
+ glyphs;
153
+ glyphLists;
154
+ famBit;
155
+ // Flat snapshots of the scene. Scene objects come in several shapes (optional fields), and
156
+ // reading their double fields at a megamorphic site boxes a fresh heap number per load; flat
157
+ // arrays keep the frame loop monomorphic and allocation-free.
158
+ nx;
159
+ ny;
160
+ nr;
161
+ nImp;
162
+ nFlow;
163
+ nLabel;
164
+ nLabelBefore;
165
+ nBadge;
166
+ eS;
167
+ eT;
168
+ eSB;
169
+ eTB;
170
+ eCurv;
171
+ eLabel;
172
+ eLabelBefore;
173
+ eUnchanged;
174
+ hNodes;
175
+ hContext;
176
+ hEdges;
177
+ hFam;
178
+ chapterMode;
179
+ chStart;
180
+ chEnd;
181
+ chHunks;
182
+ // Replay state.
183
+ wStart;
184
+ wEnd;
185
+ hState;
186
+ hP;
187
+ levelGen;
188
+ level;
189
+ edgeGen;
190
+ pulseGen;
191
+ pulseVal;
192
+ gen = 0;
193
+ env = 0;
194
+ // Selection state (cached per selected hunk).
195
+ selNode;
196
+ selHunk = -1;
197
+ // Per-frame context.
198
+ view = null;
199
+ layer = L_UNION;
200
+ famMask = 0;
201
+ reduced = false;
202
+ /** Set by style application: whether the drawn style is "changed" and its family code. */
203
+ curChanged = false;
204
+ curFamily = F_NEUTRAL;
205
+ /**
206
+ * Per-element doubles are handed to helper methods through these fields rather than as
207
+ * arguments: V8 boxes a double argument or return value at every non-inlined call, while a
208
+ * double field is updated in place.
209
+ */
210
+ u = 0;
211
+ tau = 0;
212
+ time = 0;
213
+ /** `view.emphasis`, clamped: strength of the selection spotlight. */
214
+ emph = 1;
215
+ /** Relative alpha of labels outside the selected hunk and its context (fade out as emph → 1). */
216
+ restLabels = 1;
217
+ /** Label LOD thresholds for this frame (small scenes label more). */
218
+ lodChanged = LOD.changedLabel;
219
+ lodNeutral = LOD.neutralLabel;
220
+ lodImportant = LOD.importantNeutralLabel;
221
+ /** Visible-node count per focus object (cached: a focus is immutable). */
222
+ countedFocus = null;
223
+ focusCount = 0;
224
+ px = 0;
225
+ py = 0;
226
+ pools = new WeakMap();
227
+ constructor(scene) {
228
+ this.scene = scene;
229
+ this.t = tokensFor(scene.theme);
230
+ const N = scene.nodes.length;
231
+ const M = scene.edges.length;
232
+ const H = scene.hunks.length;
233
+ this.N = N;
234
+ this.M = M;
235
+ this.H = H;
236
+ const book = new StyleBook(this.t);
237
+ this.nStyles = scene.nodes.map((n) => nodeLayerStyles(book, n));
238
+ this.eStyles = scene.edges.map((e) => edgeLayerStyles(book, e));
239
+ const hunkFam = (h) => {
240
+ const hunk = h >= 0 ? scene.hunks[h] : undefined;
241
+ return hunk === undefined ? F_NEUTRAL : familyCode(hunk.family);
242
+ };
243
+ this.nFam = Uint8Array.from(scene.nodes, (n) => familyCode(n.family));
244
+ this.nHunk = Int32Array.from(scene.nodes, (n) => n.hunk);
245
+ this.nHunkFam = Uint8Array.from(scene.nodes, (n) => hunkFam(n.hunk));
246
+ this.eFam = Uint8Array.from(scene.edges, (e) => familyCode(e.family));
247
+ this.eHunk = Int32Array.from(scene.edges, (e) => e.hunk);
248
+ this.eHunkFam = Uint8Array.from(scene.edges, (e) => hunkFam(e.hunk));
249
+ this.eUnionText = scene.edges.map((e) => e.labelBefore === undefined ? e.label : `${e.labelBefore} → ${e.label}`);
250
+ this.ghostCurv = edgeCurvatures(scene.edges).ghost;
251
+ this.famBit = Uint8Array.from([1, 2, 4, 8, 16, 0]);
252
+ this.nx = Float64Array.from(scene.nodes, (n) => n.x);
253
+ this.ny = Float64Array.from(scene.nodes, (n) => n.y);
254
+ this.nr = Float64Array.from(scene.nodes, (n) => n.r);
255
+ this.nImp = Float64Array.from(scene.nodes, (n) => n.importance);
256
+ this.nFlow = Int32Array.from(scene.nodes, (n) => n.flow ?? -1);
257
+ this.nLabel = scene.nodes.map((n) => n.label);
258
+ this.nLabelBefore = scene.nodes.map((n) => n.labelBefore ?? null);
259
+ this.nBadge = scene.nodes.map((n) => n.badge ?? null);
260
+ this.eS = Int32Array.from(scene.edges, (e) => e.s);
261
+ this.eT = Int32Array.from(scene.edges, (e) => e.t);
262
+ this.eSB = Int32Array.from(scene.edges, (e) => e.sb);
263
+ this.eTB = Int32Array.from(scene.edges, (e) => e.tb);
264
+ this.eCurv = Float64Array.from(scene.edges, (e) => e.curvature);
265
+ this.eLabel = scene.edges.map((e) => e.label);
266
+ this.eLabelBefore = scene.edges.map((e) => e.labelBefore ?? null);
267
+ this.eUnchanged = Uint8Array.from(scene.edges, (e) => (e.status === 'unchanged' ? 1 : 0));
268
+ this.hNodes = scene.hunks.map((h) => Int32Array.from(h.nodes));
269
+ this.hContext = scene.hunks.map((h) => Int32Array.from(h.context));
270
+ this.hEdges = scene.hunks.map((h) => Int32Array.from(h.edges));
271
+ this.hFam = Uint8Array.from(scene.hunks, (h) => familyCode(h.family));
272
+ this.chapterMode = scene.timeline.mode === 'chapters';
273
+ this.chStart = Float64Array.from(scene.timeline.chapters, (c) => c.start);
274
+ this.chEnd = Float64Array.from(scene.timeline.chapters, (c) => c.end);
275
+ this.chHunks = scene.timeline.chapters.map((c) => Int32Array.from(c.hunks));
276
+ // Color ramps for crossfading styles (only where the colors differ).
277
+ const bg = this.t.background;
278
+ const ramps = this.t.ramps;
279
+ this.fillRamp = [];
280
+ this.strokeRamp = [];
281
+ this.ringRamp = [];
282
+ for (let i = 0; i < N; i++) {
283
+ const [, B, A] = this.nStyles[i];
284
+ const bFill = B.hidden ? null : B.fill;
285
+ const aFill = A.hidden ? null : A.fill;
286
+ // REMOVE drains the fill into the background; FADE ramps before → after.
287
+ this.fillRamp.push(!B.hidden && A.hidden
288
+ ? ramps.get(bFill ?? bg, bg)
289
+ : bFill !== aFill && !B.hidden
290
+ ? ramps.get(bFill ?? bg, aFill ?? bg)
291
+ : null);
292
+ this.strokeRamp.push(!B.hidden && !A.hidden && B.stroke !== A.stroke && B.stroke !== null && A.stroke !== null
293
+ ? ramps.get(B.stroke, A.stroke)
294
+ : null);
295
+ this.ringRamp.push(B.ring !== null && A.ring !== null && B.ring !== A.ring ? ramps.get(B.ring, A.ring) : null);
296
+ }
297
+ this.c = choreograph(scene, this.nStyles, this.eStyles);
298
+ // Glyph specs and per-layer lists.
299
+ const specs = this.buildGlyphs();
300
+ this.glyphs = specs;
301
+ const union = [];
302
+ const after = [];
303
+ const replay = [];
304
+ specs.forEach((g, k) => {
305
+ union.push(k);
306
+ if (g.role === G_REPLACEMENT)
307
+ after.push(k);
308
+ if (g.role !== G_SUGGESTION)
309
+ replay.push(k);
310
+ });
311
+ this.glyphLists = [union, [], after, replay];
312
+ // Replay buffers.
313
+ this.wStart = new Float64Array(H);
314
+ this.wEnd = new Float64Array(H);
315
+ for (const w of scene.timeline.windows) {
316
+ if (w.hunk >= 0 && w.hunk < H) {
317
+ this.wStart[w.hunk] = w.start;
318
+ this.wEnd[w.hunk] = w.end;
319
+ }
320
+ }
321
+ this.hState = new Uint8Array(H);
322
+ this.hP = new Float64Array(H);
323
+ this.levelGen = new Uint32Array(N);
324
+ this.level = new Uint8Array(N);
325
+ this.edgeGen = new Uint32Array(M);
326
+ this.pulseGen = new Uint32Array(N);
327
+ this.pulseVal = new Float32Array(N);
328
+ this.selNode = new Uint8Array(N);
329
+ }
330
+ // ─── Construction helpers ─────────────────────────────────────────────────
331
+ buildGlyphs() {
332
+ const { scene, t } = this;
333
+ const out = [];
334
+ // One literal shape for every spec keeps reads in the frame loop monomorphic.
335
+ const specs = {
336
+ push: (g) => {
337
+ out.push({
338
+ role: g.role,
339
+ kind: g.kind,
340
+ a: g.a,
341
+ b: g.b,
342
+ edge: g.edge,
343
+ node: g.node,
344
+ hunk: g.hunk,
345
+ family: g.family,
346
+ color: g.color,
347
+ alpha: g.alpha,
348
+ width: g.width,
349
+ dash: g.dash,
350
+ curvature: g.curvature,
351
+ clearA: g.clearA,
352
+ clearB: g.clearB,
353
+ label: g.label,
354
+ });
355
+ },
356
+ };
357
+ const arc = (role, kind, a, b, node, hunk) => ({
358
+ role,
359
+ kind,
360
+ a,
361
+ b,
362
+ edge: -1,
363
+ node,
364
+ hunk,
365
+ family: F_IDENTITY,
366
+ color: t.base[F_IDENTITY],
367
+ alpha: 0.9,
368
+ width: STROKE.glyph,
369
+ dash: null,
370
+ curvature: CHOREO.arc,
371
+ clearA: 4,
372
+ clearB: 4,
373
+ label: null,
374
+ });
375
+ for (const n of scene.nodes) {
376
+ if (n.flow === undefined)
377
+ continue;
378
+ if (n.role === 'merge-source')
379
+ specs.push(arc(G_TRAIL, 'merge-arrow', n.index, n.flow, n.index, n.hunk));
380
+ else if (n.role === 'split-target')
381
+ specs.push(arc(G_TRAIL, 'split-arrow', n.flow, n.index, n.index, n.hunk));
382
+ else if (n.role === 'deprecated')
383
+ specs.push(arc(G_REPLACEMENT, 'replacement-arrow', n.index, n.flow, n.index, n.hunk));
384
+ }
385
+ for (const e of scene.edges) {
386
+ if (e.role !== 'retarget' && e.role !== 'carried')
387
+ continue;
388
+ if (e.sb === e.s && e.tb === e.t)
389
+ continue;
390
+ const fam = e.role === 'retarget' ? F_REMOVED : F_IDENTITY;
391
+ specs.push({
392
+ role: G_GHOST,
393
+ kind: 'edge-ghost',
394
+ a: e.sb,
395
+ b: e.tb,
396
+ edge: e.index,
397
+ node: -1,
398
+ hunk: e.hunk,
399
+ family: familyCode(e.family),
400
+ color: t.edge[fam],
401
+ alpha: EMPHASIS.ghostEdge,
402
+ width: STROKE.changedEdge,
403
+ dash: DASH_EDGE,
404
+ curvature: this.ghostCurv[e.index],
405
+ clearA: 0,
406
+ clearB: 0,
407
+ label: null,
408
+ });
409
+ if (e.role !== 'retarget')
410
+ continue;
411
+ const pivot = e.s === e.sb ? e.s : e.t === e.tb ? e.t : -1;
412
+ if (pivot < 0)
413
+ continue;
414
+ specs.push({
415
+ role: G_PIVOT,
416
+ kind: 'pivot',
417
+ a: pivot,
418
+ b: pivot === e.s ? e.t : e.s,
419
+ edge: e.index,
420
+ node: -1,
421
+ hunk: e.hunk,
422
+ family: F_STRUCTURE,
423
+ color: t.base[F_STRUCTURE],
424
+ alpha: 1,
425
+ width: STROKE.pivot,
426
+ dash: null,
427
+ curvature: 0,
428
+ clearA: 0,
429
+ clearB: 0,
430
+ label: null,
431
+ });
432
+ }
433
+ for (const s of scene.suggestions) {
434
+ specs.push({
435
+ role: G_SUGGESTION,
436
+ kind: 'suggestion',
437
+ a: s.before,
438
+ b: s.after,
439
+ edge: -1,
440
+ node: -1,
441
+ hunk: -1,
442
+ family: F_IDENTITY,
443
+ color: t.base[F_IDENTITY],
444
+ alpha: 0.85,
445
+ width: STROKE.suggestion,
446
+ dash: DASH_DOTTED,
447
+ curvature: 0.12,
448
+ clearA: 2,
449
+ clearB: 2,
450
+ label: '?',
451
+ });
452
+ }
453
+ return out;
454
+ }
455
+ newFrame() {
456
+ return { nodes: [], edges: [], glyphs: [], collapsed: NO_GROUPS, spotlight: 1 };
457
+ }
458
+ prepare(frame) {
459
+ let pool = this.pools.get(frame);
460
+ if (pool === undefined || pool.n !== this.N || pool.m !== this.M) {
461
+ const make = (n, f) => Array.from({ length: n }, f);
462
+ pool = {
463
+ n: this.N,
464
+ m: this.M,
465
+ rings: make(this.N, () => ({ color: '', width: 0, gap: 0, alpha: 0 })),
466
+ halos: make(this.N, () => ({ color: '', radius: 0, alpha: 0 })),
467
+ labels: make(this.N, newLabel),
468
+ secondaries: make(this.N, newLabel),
469
+ badges: make(this.N, () => ({ text: '', color: '', background: '', minZoom: 0 })),
470
+ edgeLabels: make(this.M, newLabel),
471
+ glyphs: make(this.glyphs.length, newGlyph),
472
+ glyphLabels: make(this.glyphs.length, newLabel),
473
+ };
474
+ this.pools.set(frame, pool);
475
+ }
476
+ if (frame.nodes.length !== this.N)
477
+ frame.nodes.length = this.N;
478
+ if (frame.edges.length !== this.M)
479
+ frame.edges.length = this.M;
480
+ for (let i = 0; i < this.N; i++)
481
+ if (frame.nodes[i] === undefined)
482
+ frame.nodes[i] = newNode();
483
+ for (let j = 0; j < this.M; j++)
484
+ if (frame.edges[j] === undefined)
485
+ frame.edges[j] = newEdge();
486
+ return pool;
487
+ }
488
+ // ─── Frame ────────────────────────────────────────────────────────────────
489
+ resolve(view, out) {
490
+ const frame = out ?? this.newFrame();
491
+ const pool = this.prepare(frame);
492
+ this.begin(view);
493
+ const nodes = frame.nodes;
494
+ for (let i = 0; i < this.N; i++)
495
+ this.resolveNode(i, nodes[i], pool);
496
+ const edges = frame.edges;
497
+ for (let j = 0; j < this.M; j++)
498
+ this.resolveEdge(j, edges[j], nodes, pool);
499
+ this.resolveGlyphs(frame, pool);
500
+ frame.collapsed = view.focus === null ? NO_GROUPS : view.focus.collapsed;
501
+ frame.spotlight = this.layer === L_REPLAY ? this.env : this.selHunk >= 0 ? this.emph : 0;
502
+ this.view = null;
503
+ return frame;
504
+ }
505
+ begin(view) {
506
+ this.view = view;
507
+ switch (view.layer) {
508
+ case 'union':
509
+ this.layer = L_UNION;
510
+ break;
511
+ case 'before':
512
+ this.layer = L_BEFORE;
513
+ break;
514
+ case 'after':
515
+ this.layer = L_AFTER;
516
+ break;
517
+ case 'replay':
518
+ this.layer = L_REPLAY;
519
+ break;
520
+ default:
521
+ throw new KgdiffError('invalid-input', `Unknown layer "${String(view.layer)}".`);
522
+ }
523
+ let mask = 0;
524
+ if (view.families.size > 0) {
525
+ for (let k = 0; k < CHANGE_FAMILIES.length; k++) {
526
+ if (view.families.has(CHANGE_FAMILIES[k]))
527
+ mask |= 1 << k;
528
+ }
529
+ }
530
+ this.famMask = mask;
531
+ this.reduced = view.reducedMotion;
532
+ const e = view.emphasis;
533
+ this.emph = Number.isFinite(e) ? clamp01(e) : 1;
534
+ this.restLabels = 1 - this.emph;
535
+ const focus = view.focus;
536
+ if (focus !== null &&
537
+ (focus.nodes.length !== this.N ||
538
+ focus.edges.length !== this.M ||
539
+ focus.context.length !== this.N)) {
540
+ throw new KgdiffError('invalid-input', 'view.focus does not belong to this scene (array lengths differ); recompute it with computeFocus(scene).');
541
+ }
542
+ const visibleNodes = focus === null ? this.N : this.focusVisible(focus);
543
+ const small = visibleNodes <= LOD.smallScene;
544
+ this.lodChanged = small ? LOD.smallChangedLabel : LOD.changedLabel;
545
+ this.lodNeutral = small ? LOD.smallNeutralLabel : LOD.neutralLabel;
546
+ this.lodImportant = small ? LOD.smallNeutralLabel : LOD.importantNeutralLabel;
547
+ this.updateSelection(view.selectedHunk >= 0 && view.selectedHunk < this.H ? view.selectedHunk : -1);
548
+ this.env = 0;
549
+ if (this.layer === L_REPLAY) {
550
+ this.time = Number.isFinite(view.t) ? view.t : 0;
551
+ this.updateReplay();
552
+ }
553
+ }
554
+ focusVisible(focus) {
555
+ if (focus !== this.countedFocus) {
556
+ let n = 0;
557
+ const nodes = focus.nodes;
558
+ for (let i = 0; i < nodes.length; i++)
559
+ if (nodes[i] === 1)
560
+ n++;
561
+ this.countedFocus = focus;
562
+ this.focusCount = n;
563
+ }
564
+ return this.focusCount;
565
+ }
566
+ updateSelection(h) {
567
+ if (h === this.selHunk)
568
+ return;
569
+ this.selNode.fill(0);
570
+ if (h >= 0) {
571
+ const context = this.hContext[h];
572
+ for (let k = 0; k < context.length; k++)
573
+ this.selNode[context[k]] = 1;
574
+ const nodes = this.hNodes[h];
575
+ for (let k = 0; k < nodes.length; k++)
576
+ this.selNode[nodes[k]] = 2;
577
+ }
578
+ this.selHunk = h;
579
+ }
580
+ updateReplay() {
581
+ const { H, hState, hP, wStart, wEnd } = this;
582
+ const t = this.time;
583
+ let active = -1;
584
+ for (let h = 0; h < H; h++) {
585
+ const s = wStart[h];
586
+ const e = wEnd[h];
587
+ if (t < s)
588
+ hState[h] = 0;
589
+ else if (t >= e)
590
+ hState[h] = 2;
591
+ else {
592
+ hState[h] = 1;
593
+ hP[h] = (t - s) / (e - s);
594
+ active = h;
595
+ }
596
+ }
597
+ this.gen = (this.gen + 1) >>> 0;
598
+ if (this.gen === 0) {
599
+ this.levelGen.fill(0);
600
+ this.edgeGen.fill(0);
601
+ this.pulseGen.fill(0);
602
+ this.gen = 1;
603
+ }
604
+ if (!this.chapterMode) {
605
+ if (active >= 0) {
606
+ // Ramps in over the spotlight phase and out over the settle phase.
607
+ const p = hP[active];
608
+ this.env =
609
+ p < SPOT_END
610
+ ? easeStandard(p / SPOT_END)
611
+ : p > SETTLE
612
+ ? easeStandard((1 - p) / (1 - SETTLE))
613
+ : 1;
614
+ this.markHunk(active);
615
+ }
616
+ return;
617
+ }
618
+ // Chapters: the whole chapter is spotlighted, ramping over its first window's spotlight phase.
619
+ for (let c = 0; c < this.chHunks.length; c++) {
620
+ const start = this.chStart[c];
621
+ const end = this.chEnd[c];
622
+ if (t < start || t >= end)
623
+ continue;
624
+ const hunks = this.chHunks[c];
625
+ const first = hunks.length > 0 ? hunks[0] : -1;
626
+ const ramp = first < 0 ? 1 : SPOT_END * (wEnd[first] - wStart[first]);
627
+ this.env = easeStandard(clamp01(Math.min(t - start, end - t) / Math.max(ramp, 1e-9)));
628
+ for (let k = 0; k < hunks.length; k++)
629
+ this.markHunk(hunks[k]);
630
+ break;
631
+ }
632
+ }
633
+ /** Emphasize a hunk for the replay spotlight and schedule its pulse. */
634
+ markHunk(h) {
635
+ const context = this.hContext[h];
636
+ const nodes = this.hNodes[h];
637
+ const edges = this.hEdges[h];
638
+ if (context === undefined || nodes === undefined || edges === undefined)
639
+ return;
640
+ const { gen, levelGen, level, pulseGen, pulseVal } = this;
641
+ const pulsing = this.hState[h] === 1 && this.hP[h] < SPOT_END && !this.reduced;
642
+ const pulse = pulsing ? this.hP[h] / SPOT_END : 0;
643
+ for (let k = 0; k < context.length; k++) {
644
+ const i = context[k];
645
+ if (levelGen[i] !== gen) {
646
+ levelGen[i] = gen;
647
+ level[i] = 1;
648
+ }
649
+ if (pulsing && this.c.hPulseContext[h] === 1) {
650
+ pulseGen[i] = gen;
651
+ pulseVal[i] = pulse;
652
+ }
653
+ }
654
+ for (let k = 0; k < nodes.length; k++) {
655
+ const i = nodes[k];
656
+ levelGen[i] = gen;
657
+ level[i] = 2;
658
+ if (pulsing && this.c.nPulse[i] === 1) {
659
+ pulseGen[i] = gen;
660
+ pulseVal[i] = pulse;
661
+ }
662
+ }
663
+ for (let k = 0; k < edges.length; k++)
664
+ this.edgeGen[edges[k]] = gen;
665
+ }
666
+ /** Spotlight level of a node: 2 emphasized hunk, 1 its context, 0 the rest. */
667
+ nodeLevel(i) {
668
+ if (this.layer === L_REPLAY)
669
+ return this.levelGen[i] === this.gen ? this.level[i] : 0;
670
+ return this.selNode[i];
671
+ }
672
+ /**
673
+ * Alpha factor of the spotlight for an element at `level`: the replay spotlight ramps with its
674
+ * window envelope, the selection spotlight with `view.emphasis`.
675
+ */
676
+ spotlight(level) {
677
+ if (this.layer === L_REPLAY) {
678
+ if (this.env <= 0 || level === 2)
679
+ return 1;
680
+ return lerp(1, level === 1 ? EMPHASIS.replayContext : EMPHASIS.replayRest, this.env);
681
+ }
682
+ if (this.selHunk < 0 || level === 2)
683
+ return 1;
684
+ return lerp(1, level === 1 ? EMPHASIS.selectedContext : EMPHASIS.selectedRest, this.emph);
685
+ }
686
+ /** Labels outside the selected hunk and its context fade out with the selection spotlight. */
687
+ fadesRestLabels(level) {
688
+ return this.layer !== L_REPLAY && this.selHunk >= 0 && level === 0;
689
+ }
690
+ filtered(fam, hunkFam) {
691
+ const mask = this.famMask;
692
+ return (mask !== 0 &&
693
+ (this.famBit[fam] & mask) === 0 &&
694
+ (this.famBit[hunkFam] & mask) === 0);
695
+ }
696
+ // ─── Nodes ────────────────────────────────────────────────────────────────
697
+ resolveNode(i, v, pool) {
698
+ v.x = this.nx[i];
699
+ v.y = this.ny[i];
700
+ v.r = this.nr[i];
701
+ v.family = FAMILY_BY_CODE[this.nFam[i]];
702
+ v.pulse = 0;
703
+ v.strokeWidth = STROKE.node;
704
+ const styles = this.nStyles[i];
705
+ let ok;
706
+ if (this.layer !== L_REPLAY)
707
+ ok = this.applyNode(v, i, styles[this.layer], pool);
708
+ else {
709
+ const h = this.nHunk[i];
710
+ const state = h < 0 ? 2 : this.hState[h];
711
+ if (state === 0)
712
+ ok = this.applyNode(v, i, styles[L_BEFORE], pool);
713
+ else if (state === 2)
714
+ ok = this.applyNode(v, i, styles[L_AFTER], pool);
715
+ else {
716
+ const p = this.hP[h];
717
+ if (p < SPOT_END)
718
+ ok = this.applyNode(v, i, styles[L_BEFORE], pool);
719
+ else if (p >= SETTLE)
720
+ ok = this.applyNode(v, i, styles[L_AFTER], pool);
721
+ else {
722
+ this.u = (p - SPOT_END) / TRANSFORM;
723
+ ok = this.animateNode(v, i, styles, pool);
724
+ }
725
+ }
726
+ if (ok && this.pulseGen[i] === this.gen)
727
+ v.pulse = this.pulseVal[i];
728
+ }
729
+ if (!ok) {
730
+ hideNode(v);
731
+ return;
732
+ }
733
+ this.modulateNode(v, i, pool);
734
+ }
735
+ /** Copy a static style into `v` (position / radius already set). Returns visibility. */
736
+ applyNode(v, i, st, pool) {
737
+ if (st.hidden)
738
+ return false;
739
+ const t = this.t;
740
+ const before = this.nLabelBefore[i] ?? null;
741
+ v.visible = true;
742
+ v.alpha = 1;
743
+ v.family = FAMILY_BY_CODE[st.family];
744
+ v.fill = st.fill;
745
+ v.stroke = st.stroke;
746
+ v.dash = st.dash;
747
+ v.z = st.z;
748
+ if (st.ring !== null) {
749
+ const ring = pool.rings[i];
750
+ ring.color = st.ring;
751
+ ring.width = STROKE.ring;
752
+ ring.gap = STROKE.ringGap;
753
+ ring.alpha = 1;
754
+ v.ring = ring;
755
+ }
756
+ else
757
+ v.ring = null;
758
+ if (st.halo !== null) {
759
+ const halo = pool.halos[i];
760
+ halo.color = st.halo;
761
+ halo.radius = v.r * t.haloScale;
762
+ halo.alpha = t.haloAlpha;
763
+ v.halo = halo;
764
+ }
765
+ else
766
+ v.halo = null;
767
+ const label = pool.labels[i];
768
+ label.text = st.labelBefore && before !== null ? before : this.nLabel[i];
769
+ label.color = st.labelColor;
770
+ label.alpha = 1;
771
+ label.size = LABEL.size;
772
+ label.weight = st.labelWeight;
773
+ label.strike = st.strike;
774
+ label.offset = LABEL.offset;
775
+ v.label = label;
776
+ if (st.secondary && before !== null) {
777
+ const sec = pool.secondaries[i];
778
+ sec.text = before;
779
+ sec.color = t.muted;
780
+ sec.alpha = 1;
781
+ sec.size = LABEL.oldSize;
782
+ sec.weight = t.weightRegular;
783
+ sec.strike = true;
784
+ sec.offset = LABEL.offset + LABEL.secondaryGap;
785
+ v.secondaryLabel = sec;
786
+ }
787
+ else
788
+ v.secondaryLabel = null;
789
+ const badge = this.nBadge[i] ?? null;
790
+ if (st.badge && badge !== null)
791
+ this.setBadge(v, i, badge, st.family, pool);
792
+ else
793
+ v.badge = null;
794
+ this.curChanged = st.changed;
795
+ this.curFamily = st.family;
796
+ return true;
797
+ }
798
+ setBadge(v, i, text, family, pool) {
799
+ const badge = pool.badges[i];
800
+ badge.text = text;
801
+ badge.color = this.t.text[family];
802
+ badge.background = this.t.soft[family];
803
+ v.badge = badge;
804
+ }
805
+ animateNode(v, i, styles, pool) {
806
+ const c = this.c;
807
+ const u = this.u;
808
+ const kind = c.nKind[i];
809
+ const B = styles[L_BEFORE];
810
+ const A = styles[L_AFTER];
811
+ const reduced = this.reduced;
812
+ switch (kind) {
813
+ case NA_FADE:
814
+ return this.animFade(v, i, B, A, pool);
815
+ case NA_REMOVE:
816
+ case NA_TRAVEL: {
817
+ if (!this.applyNode(v, i, B, pool))
818
+ return false;
819
+ if (reduced) {
820
+ v.alpha = 1 - easeStandard(u);
821
+ return v.alpha > 0;
822
+ }
823
+ return kind === NA_REMOVE ? this.animRemove(v, i, B) : this.animTravel(v, i);
824
+ }
825
+ case NA_ADD:
826
+ case NA_EMERGE: {
827
+ if (!this.applyNode(v, i, A, pool))
828
+ return false;
829
+ if (reduced) {
830
+ v.alpha = easeStandard(u);
831
+ return v.alpha > 0;
832
+ }
833
+ return kind === NA_ADD ? this.animAdd(v, i) : this.animEmerge(v, i);
834
+ }
835
+ case NA_NONE:
836
+ return this.applyNode(v, i, u < 0.5 ? B : A, pool);
837
+ default:
838
+ return this.applyNode(v, i, A, pool);
839
+ }
840
+ }
841
+ animFade(v, i, B, A, pool) {
842
+ const c = this.c;
843
+ const u = this.u;
844
+ const labelBefore = this.nLabelBefore[i] ?? null;
845
+ const badge = this.nBadge[i] ?? null;
846
+ const reduced = this.reduced;
847
+ const k = reduced
848
+ ? easeStandard(u)
849
+ : easeStandard(segment(u, c.nA[i], c.nB[i]));
850
+ if (!this.applyNode(v, i, k < 0.5 ? B : A, pool))
851
+ return false;
852
+ const t = this.t;
853
+ // Colors.
854
+ const fillRamp = this.fillRamp[i] ?? null;
855
+ if (fillRamp !== null && k > 0 && k < 1)
856
+ v.fill = rampAt(fillRamp, k);
857
+ const strokeRamp = this.strokeRamp[i] ?? null;
858
+ if (strokeRamp !== null && k > 0 && k < 1)
859
+ v.stroke = rampAt(strokeRamp, k);
860
+ // Ring: color ramp when both have one, else fade in / out.
861
+ const ring = pool.rings[i];
862
+ if (B.ring !== null || A.ring !== null) {
863
+ const ringRamp = this.ringRamp[i] ?? null;
864
+ ring.color =
865
+ A.ring !== null && B.ring !== null
866
+ ? ringRamp !== null
867
+ ? rampAt(ringRamp, k)
868
+ : A.ring
869
+ : (A.ring ?? B.ring ?? '');
870
+ ring.width = STROKE.ring;
871
+ ring.gap = STROKE.ringGap;
872
+ ring.alpha = A.ring !== null && B.ring !== null ? 1 : A.ring !== null ? k : 1 - k;
873
+ v.ring = ring.alpha > 0 ? ring : null;
874
+ }
875
+ if (!reduced && c.nFlashA[i] >= 0 && v.ring !== null) {
876
+ const f = bump(segment(u, c.nFlashA[i], c.nFlashB[i]));
877
+ ring.width = STROKE.ring + STROKE.ring * f;
878
+ ring.alpha = Math.max(ring.alpha, f);
879
+ }
880
+ // Halo.
881
+ if (B.halo !== null || A.halo !== null) {
882
+ const halo = pool.halos[i];
883
+ halo.color = (A.halo ?? B.halo);
884
+ halo.radius = v.r * t.haloScale;
885
+ halo.alpha =
886
+ t.haloAlpha * (A.halo !== null && B.halo !== null ? 1 : A.halo !== null ? k : 1 - k);
887
+ v.halo = halo.alpha > 0 ? halo : null;
888
+ }
889
+ // Label: the old name slides up and fades while the new one rises in (crossfade in place
890
+ // with reduced motion).
891
+ if (c.nLabelSwap[i] === 1 && labelBefore !== null) {
892
+ const kOut = reduced ? k : easeStandard(segment(u, CHOREO.labelOut[0], CHOREO.labelOut[1]));
893
+ const kIn = reduced ? k : easeStandard(segment(u, CHOREO.labelIn[0], CHOREO.labelIn[1]));
894
+ const label = pool.labels[i];
895
+ label.text = this.nLabel[i];
896
+ label.color = A.labelColor;
897
+ label.weight = A.labelWeight;
898
+ label.strike = A.strike;
899
+ label.alpha = kIn;
900
+ label.offset = LABEL.offset + (reduced ? 0 : CHOREO.labelSlide * (1 - kIn));
901
+ v.label = label;
902
+ const old = pool.secondaries[i];
903
+ old.text = labelBefore;
904
+ old.color = B.labelColor;
905
+ old.size = LABEL.size;
906
+ old.weight = B.labelWeight;
907
+ old.strike = false;
908
+ old.alpha = 1 - kOut;
909
+ old.offset = LABEL.offset - (reduced ? 0 : CHOREO.labelSlide * kOut);
910
+ v.secondaryLabel = old.alpha > 0 ? old : null;
911
+ }
912
+ // Badge: ticks in; merge targets count their arrivals up.
913
+ const arrivals = c.nArrivals[i] ?? null;
914
+ const steps = c.nBadgeSteps[i] ?? null;
915
+ if (arrivals !== null && steps !== null) {
916
+ let count = 0;
917
+ if (reduced)
918
+ count = k >= 0.5 ? arrivals.length : 0;
919
+ else
920
+ while (count < arrivals.length && arrivals[count] <= u)
921
+ count++;
922
+ if (count > 0)
923
+ this.setBadge(v, i, steps[count - 1], A.family, pool);
924
+ else
925
+ v.badge = null;
926
+ }
927
+ else if (c.nBadgeAt[i] <= 1 && badge !== null) {
928
+ if (reduced ? k >= 0.5 : u >= c.nBadgeAt[i])
929
+ this.setBadge(v, i, badge, A.family, pool);
930
+ else
931
+ v.badge = null;
932
+ }
933
+ return true;
934
+ }
935
+ animRemove(v, i, B) {
936
+ const c = this.c;
937
+ const u = this.u;
938
+ const s = easeStandard(segment(u, c.nA[i], c.nB[i]));
939
+ if (s > 0) {
940
+ v.r = this.nr[i] * (1 - 0.4 * s);
941
+ const ramp = this.fillRamp[i] ?? null;
942
+ v.fill = s >= 1 || ramp === null ? null : rampAt(ramp, s);
943
+ v.dash = DASH_GHOST;
944
+ v.stroke = this.t.base[B.family];
945
+ if (v.label !== null)
946
+ v.label.strike = true;
947
+ if (v.ring !== null)
948
+ v.ring.alpha = 1 - s;
949
+ if (v.halo !== null) {
950
+ v.halo.alpha *= 1 - s;
951
+ v.halo.radius = v.r * this.t.haloScale;
952
+ }
953
+ }
954
+ v.alpha = 1 - easeStandard(segment(u, c.nC[i], 1));
955
+ return v.alpha > 0;
956
+ }
957
+ /** Point at `this.tau` on the travel arc from node `from` to node `to`, into (px, py). */
958
+ arcPoint(from, to) {
959
+ const { nx, ny } = this;
960
+ const x0 = nx[from];
961
+ const y0 = ny[from];
962
+ const x1 = nx[to];
963
+ const y1 = ny[to];
964
+ const tau = this.tau;
965
+ const cx = (x0 + x1) / 2 - CHOREO.arc * (y1 - y0);
966
+ const cy = (y0 + y1) / 2 + CHOREO.arc * (x1 - x0);
967
+ const a = (1 - tau) * (1 - tau);
968
+ const b = 2 * (1 - tau) * tau;
969
+ const d = tau * tau;
970
+ this.px = a * x0 + b * cx + d * x1;
971
+ this.py = a * y0 + b * cy + d * y1;
972
+ }
973
+ animTravel(v, i) {
974
+ const c = this.c;
975
+ const u = this.u;
976
+ const target = this.nFlow[i];
977
+ if (target < 0)
978
+ return true;
979
+ const a = c.nA[i];
980
+ const b = c.nB[i];
981
+ this.tau = easeInOut(segment(u, a, b));
982
+ this.arcPoint(i, target);
983
+ v.x = this.px;
984
+ v.y = this.py;
985
+ v.z = 3;
986
+ v.alpha = 1 - segment(u, b, b + CHOREO.arrivalFade);
987
+ if (v.label !== null)
988
+ v.label.alpha = 1 - segment(u, (a + b) / 2, b);
989
+ if (v.halo !== null)
990
+ v.halo.radius = v.r * this.t.haloScale;
991
+ return v.alpha > 0;
992
+ }
993
+ animAdd(v, i) {
994
+ const c = this.c;
995
+ const u = this.u;
996
+ const pop = segment(u, c.nA[i], c.nB[i]);
997
+ v.r = pop > 0 ? this.nr[i] * backOut(pop) : 0;
998
+ if (pop <= 0)
999
+ return false;
1000
+ v.alpha = Math.min(1, pop * 4);
1001
+ if (v.halo !== null)
1002
+ v.halo.radius = v.r * this.t.haloScale;
1003
+ const labelStart = c.nC[i];
1004
+ const la = easeStandard(segment(u, labelStart, Math.min(1, labelStart + CHOREO.labelFadeIn)));
1005
+ if (v.label !== null)
1006
+ v.label.alpha = la;
1007
+ if (v.secondaryLabel !== null)
1008
+ v.secondaryLabel.alpha = la;
1009
+ if (la < 1)
1010
+ v.badge = null;
1011
+ return true;
1012
+ }
1013
+ animEmerge(v, i) {
1014
+ const c = this.c;
1015
+ const u = this.u;
1016
+ const source = this.nFlow[i];
1017
+ if (source < 0)
1018
+ return true;
1019
+ const a = c.nA[i];
1020
+ const b = c.nB[i];
1021
+ const tau = easeInOut(segment(u, a, b));
1022
+ this.tau = tau;
1023
+ this.arcPoint(source, i);
1024
+ v.x = this.px;
1025
+ v.y = this.py;
1026
+ v.r = this.nr[i] * (0.5 + 0.5 * tau);
1027
+ v.z = 3;
1028
+ v.alpha = segment(u, a, a + 0.1);
1029
+ if (v.halo !== null)
1030
+ v.halo.radius = v.r * this.t.haloScale;
1031
+ const la = easeStandard(segment(u, b - 0.05, Math.min(1, b + CHOREO.labelFadeIn)));
1032
+ if (v.label !== null)
1033
+ v.label.alpha = la;
1034
+ if (v.secondaryLabel !== null)
1035
+ v.secondaryLabel.alpha = la;
1036
+ if (la < 1)
1037
+ v.badge = null;
1038
+ return v.alpha > 0;
1039
+ }
1040
+ modulateNode(v, i, pool) {
1041
+ const view = this.view;
1042
+ const t = this.t;
1043
+ let f = 1;
1044
+ const focus = view.focus;
1045
+ if (focus !== null) {
1046
+ if (focus.nodes[i] !== 1) {
1047
+ hideNode(v);
1048
+ return;
1049
+ }
1050
+ if (focus.context[i] === 1)
1051
+ f *= EMPHASIS.focusContext;
1052
+ }
1053
+ let changed = this.curChanged;
1054
+ let family = this.curFamily;
1055
+ let neutral = false;
1056
+ if (this.filtered(this.nFam[i], this.nHunkFam[i])) {
1057
+ neutral = true;
1058
+ changed = false;
1059
+ family = F_NEUTRAL;
1060
+ v.family = 'neutral';
1061
+ if (v.fill !== null)
1062
+ v.fill = t.base[F_NEUTRAL];
1063
+ v.stroke = t.strong[F_NEUTRAL];
1064
+ v.ring = null;
1065
+ v.halo = null;
1066
+ v.badge = null;
1067
+ v.secondaryLabel = null;
1068
+ if (v.label !== null) {
1069
+ v.label.color = t.neutralLabel;
1070
+ v.label.weight = t.weightRegular;
1071
+ }
1072
+ f *= EMPHASIS.filtered;
1073
+ }
1074
+ const level = this.nodeLevel(i);
1075
+ f *= this.spotlight(level);
1076
+ const selLevel = this.selHunk >= 0 ? this.selNode[i] : 0;
1077
+ const fadeLabels = this.fadesRestLabels(selLevel);
1078
+ // The selected hunk's elements get halos (fading in with the emphasis).
1079
+ if (selLevel === 2 && !neutral && v.halo === null && this.emph > 0) {
1080
+ const halo = pool.halos[i];
1081
+ halo.color = t.base[family];
1082
+ halo.radius = v.r * t.haloScale;
1083
+ halo.alpha = t.haloAlpha * this.emph;
1084
+ v.halo = halo;
1085
+ }
1086
+ const hovered = view.hoveredNode === i;
1087
+ const picked = view.selectedNode === i;
1088
+ if (hovered) {
1089
+ v.strokeWidth = STROKE.hoveredNode;
1090
+ v.stroke = changed ? t.strong[family] : t.secondary;
1091
+ v.z = 4;
1092
+ }
1093
+ if (picked) {
1094
+ const ring = pool.rings[i];
1095
+ ring.color = t.accent;
1096
+ ring.width = STROKE.hoveredNode;
1097
+ ring.gap = STROKE.ringGap;
1098
+ ring.alpha = 1;
1099
+ v.ring = ring;
1100
+ v.z = 4;
1101
+ }
1102
+ v.alpha *= f;
1103
+ // LOD and collision priority.
1104
+ const imp = this.nImp[i];
1105
+ let minZoom = changed
1106
+ ? this.lodChanged
1107
+ : imp >= LOD.importantThreshold
1108
+ ? this.lodImportant
1109
+ : this.lodNeutral;
1110
+ let priority = changed ? 1 + imp : imp;
1111
+ if (hovered) {
1112
+ minZoom = 0;
1113
+ priority = 2 + imp;
1114
+ }
1115
+ if (selLevel === 2 || picked) {
1116
+ minZoom = 0;
1117
+ priority = 3 + imp;
1118
+ }
1119
+ if (fadeLabels && !hovered && !picked) {
1120
+ const rest = this.restLabels;
1121
+ if (rest <= 0) {
1122
+ v.label = null;
1123
+ v.secondaryLabel = null;
1124
+ }
1125
+ else {
1126
+ if (v.label !== null)
1127
+ v.label.alpha *= rest;
1128
+ if (v.secondaryLabel !== null)
1129
+ v.secondaryLabel.alpha *= rest;
1130
+ }
1131
+ // Badges have no alpha of their own: drop them halfway through the fade.
1132
+ if (rest < 0.5)
1133
+ v.badge = null;
1134
+ }
1135
+ // Nested alphas stay relative to v.alpha (renderers multiply).
1136
+ if (v.label !== null) {
1137
+ v.label.minZoom = minZoom;
1138
+ v.label.priority = priority;
1139
+ }
1140
+ if (v.secondaryLabel !== null) {
1141
+ v.secondaryLabel.minZoom = minZoom;
1142
+ v.secondaryLabel.priority = priority - 0.5;
1143
+ }
1144
+ if (v.badge !== null)
1145
+ v.badge.minZoom = selLevel === 2 ? 0 : LOD.badge;
1146
+ }
1147
+ // ─── Edges ────────────────────────────────────────────────────────────────
1148
+ resolveEdge(j, ev, nodes, pool) {
1149
+ const styles = this.eStyles[j];
1150
+ let ok;
1151
+ if (this.layer !== L_REPLAY)
1152
+ ok = this.applyEdge(ev, j, styles[this.layer], nodes, pool);
1153
+ else {
1154
+ const h = this.eHunk[j];
1155
+ const state = h < 0 ? 2 : this.hState[h];
1156
+ if (state === 0)
1157
+ ok = this.applyEdge(ev, j, styles[L_BEFORE], nodes, pool);
1158
+ else if (state === 2)
1159
+ ok = this.applyEdge(ev, j, styles[L_AFTER], nodes, pool);
1160
+ else {
1161
+ const p = this.hP[h];
1162
+ if (p < SPOT_END)
1163
+ ok = this.applyEdge(ev, j, styles[L_BEFORE], nodes, pool);
1164
+ else if (p >= SETTLE)
1165
+ ok = this.applyEdge(ev, j, styles[L_AFTER], nodes, pool);
1166
+ else {
1167
+ this.u = (p - SPOT_END) / TRANSFORM;
1168
+ ok = this.animateEdge(ev, j, styles, nodes, pool);
1169
+ }
1170
+ }
1171
+ }
1172
+ if (!ok) {
1173
+ hideEdge(ev);
1174
+ return;
1175
+ }
1176
+ this.modulateEdge(ev, j, pool);
1177
+ }
1178
+ setEnds(ev, a, b) {
1179
+ ev.x1 = a.x;
1180
+ ev.y1 = a.y;
1181
+ ev.r1 = a.r;
1182
+ ev.x2 = b.x;
1183
+ ev.y2 = b.y;
1184
+ ev.r2 = b.r;
1185
+ }
1186
+ applyEdge(ev, j, st, nodes, pool) {
1187
+ if (st.hidden)
1188
+ return false;
1189
+ const es = this.eS[j];
1190
+ const et = this.eT[j];
1191
+ const esb = this.eSB[j];
1192
+ const etb = this.eTB[j];
1193
+ const before = st.before && (esb !== es || etb !== et);
1194
+ this.setEnds(ev, nodes[before ? esb : es], nodes[before ? etb : et]);
1195
+ ev.curvature = (before ? this.ghostCurv[j] : this.eCurv[j]);
1196
+ ev.visible = true;
1197
+ ev.color = st.color;
1198
+ ev.width = st.width;
1199
+ ev.alpha = st.alpha;
1200
+ ev.dash = st.dash;
1201
+ ev.dashOffset = 0;
1202
+ ev.arrow = st.arrow;
1203
+ ev.arrowMinZoom = st.changed ? 0 : LOD.arrow;
1204
+ ev.progress = 1;
1205
+ ev.fromTarget = false;
1206
+ ev.z = st.z;
1207
+ if (st.label === EL_NONE)
1208
+ ev.label = null;
1209
+ else {
1210
+ const label = pool.edgeLabels[j];
1211
+ label.text =
1212
+ st.label === EL_UNION
1213
+ ? this.eUnionText[j]
1214
+ : st.label === EL_BEFORE
1215
+ ? (this.eLabelBefore[j] ?? this.eLabel[j])
1216
+ : this.eLabel[j];
1217
+ label.color = st.labelColor;
1218
+ label.alpha = 1;
1219
+ label.size = LABEL.edgeSize;
1220
+ label.weight = this.t.weightRegular;
1221
+ label.strike = st.strike;
1222
+ label.offset = 0;
1223
+ ev.label = label;
1224
+ }
1225
+ this.curChanged = st.changed;
1226
+ this.curFamily = st.family;
1227
+ return true;
1228
+ }
1229
+ animateEdge(ev, j, styles, nodes, pool) {
1230
+ const c = this.c;
1231
+ const u = this.u;
1232
+ const B = styles[L_BEFORE];
1233
+ const A = styles[L_AFTER];
1234
+ const a = c.eA[j];
1235
+ const b = c.eB[j];
1236
+ const reduced = this.reduced;
1237
+ switch (c.eKind[j]) {
1238
+ case EA_GROW: {
1239
+ if (!this.applyEdge(ev, j, A, nodes, pool))
1240
+ return false;
1241
+ if (reduced) {
1242
+ ev.alpha *= easeStandard(u);
1243
+ return ev.alpha > 0;
1244
+ }
1245
+ const g = easeStandard(segment(u, a, b));
1246
+ if (g <= 0)
1247
+ return false;
1248
+ ev.progress = g;
1249
+ ev.fromTarget = c.eFromTarget[j] === 1;
1250
+ if (ev.label !== null)
1251
+ ev.label.alpha = easeStandard(segment(u, b - 0.05, Math.min(1, b + 0.15)));
1252
+ return true;
1253
+ }
1254
+ case EA_RETRACT: {
1255
+ if (!this.applyEdge(ev, j, B, nodes, pool))
1256
+ return false;
1257
+ if (reduced) {
1258
+ ev.alpha *= 1 - easeStandard(u);
1259
+ return ev.alpha > 0;
1260
+ }
1261
+ const g = easeStandard(segment(u, a, b));
1262
+ if (g >= 1)
1263
+ return false;
1264
+ ev.progress = 1 - g;
1265
+ ev.fromTarget = c.eFromTarget[j] === 1;
1266
+ if (ev.label !== null)
1267
+ ev.label.alpha = 1 - segment(u, a, a + 0.1);
1268
+ return true;
1269
+ }
1270
+ case EA_SWING: {
1271
+ if (!this.applyEdge(ev, j, A, nodes, pool))
1272
+ return false;
1273
+ if (reduced) {
1274
+ ev.alpha *= easeStandard(u);
1275
+ return ev.alpha > 0;
1276
+ }
1277
+ this.tau = easeInOut(segment(u, a, b));
1278
+ this.swing(ev, j, nodes);
1279
+ return true;
1280
+ }
1281
+ case EA_FOLLOW_MERGE: {
1282
+ if (reduced) {
1283
+ if (A.hidden) {
1284
+ if (!this.applyEdge(ev, j, B, nodes, pool))
1285
+ return false;
1286
+ ev.alpha *= 1 - easeStandard(u);
1287
+ }
1288
+ else {
1289
+ if (!this.applyEdge(ev, j, A, nodes, pool))
1290
+ return false;
1291
+ ev.alpha *= easeStandard(u);
1292
+ }
1293
+ return ev.alpha > 0;
1294
+ }
1295
+ if (u < b || A.hidden) {
1296
+ // Rides the travelling source (before form reads its animated position).
1297
+ if (!this.applyEdge(ev, j, B, nodes, pool))
1298
+ return false;
1299
+ ev.alpha *= 1 - segment(u, b, b + CHOREO.arrivalFade);
1300
+ return ev.alpha > 0;
1301
+ }
1302
+ return this.applyEdge(ev, j, A, nodes, pool);
1303
+ }
1304
+ case EA_FOLLOW_SPLIT: {
1305
+ if (!this.applyEdge(ev, j, A, nodes, pool))
1306
+ return false;
1307
+ if (reduced)
1308
+ ev.alpha *= easeStandard(u);
1309
+ return ev.alpha > 0;
1310
+ }
1311
+ case EA_FADE: {
1312
+ const k = easeStandard(segment(u, a, b));
1313
+ if (!this.applyEdge(ev, j, k < 0.5 ? B : A, nodes, pool))
1314
+ return false;
1315
+ if (ev.label !== null && B.label !== A.label)
1316
+ ev.label.alpha = Math.abs(1 - 2 * k);
1317
+ if (!reduced)
1318
+ ev.width += bump(k);
1319
+ return true;
1320
+ }
1321
+ case EA_SWAP: {
1322
+ if (!this.applyEdge(ev, j, A, nodes, pool))
1323
+ return false;
1324
+ ev.alpha *= easeStandard(reduced ? u : segment(u, a, b));
1325
+ return ev.alpha > 0;
1326
+ }
1327
+ case EA_NONE:
1328
+ return this.applyEdge(ev, j, u < 0.5 ? B : A, nodes, pool);
1329
+ default:
1330
+ return this.applyEdge(ev, j, A, nodes, pool);
1331
+ }
1332
+ }
1333
+ /** Swing the changed end around the pivot (polar interpolation) at progress `this.tau`. */
1334
+ swing(ev, j, nodes) {
1335
+ const s = this.tau;
1336
+ const pivot = this.c.ePivot[j];
1337
+ const fixedIsSource = pivot === this.eS[j];
1338
+ const from = nodes[(fixedIsSource ? this.eTB[j] : this.eSB[j])];
1339
+ const to = nodes[(fixedIsSource ? this.eT[j] : this.eS[j])];
1340
+ const p = nodes[pivot];
1341
+ const a0 = Math.atan2(from.y - p.y, from.x - p.x);
1342
+ let da = Math.atan2(to.y - p.y, to.x - p.x) - a0;
1343
+ if (da > Math.PI)
1344
+ da -= 2 * Math.PI;
1345
+ else if (da < -Math.PI)
1346
+ da += 2 * Math.PI;
1347
+ const rho = lerp(Math.hypot(from.x - p.x, from.y - p.y), Math.hypot(to.x - p.x, to.y - p.y), s);
1348
+ const angle = a0 + da * s;
1349
+ const x = p.x + rho * Math.cos(angle);
1350
+ const y = p.y + rho * Math.sin(angle);
1351
+ const r = lerp(from.r, to.r, s);
1352
+ if (fixedIsSource) {
1353
+ ev.x2 = x;
1354
+ ev.y2 = y;
1355
+ ev.r2 = r;
1356
+ }
1357
+ else {
1358
+ ev.x1 = x;
1359
+ ev.y1 = y;
1360
+ ev.r1 = r;
1361
+ }
1362
+ ev.curvature = lerp(this.ghostCurv[j], this.eCurv[j], s);
1363
+ }
1364
+ modulateEdge(ev, j, pool) {
1365
+ const view = this.view;
1366
+ const es = this.eS[j];
1367
+ const et = this.eT[j];
1368
+ const t = this.t;
1369
+ let f = 1;
1370
+ const focus = view.focus;
1371
+ if (focus !== null) {
1372
+ if (focus.edges[j] !== 1) {
1373
+ hideEdge(ev);
1374
+ return;
1375
+ }
1376
+ if (this.eUnchanged[j] === 1 && (focus.context[es] === 1 || focus.context[et] === 1)) {
1377
+ f *= EMPHASIS.focusContext;
1378
+ }
1379
+ }
1380
+ let changed = this.curChanged;
1381
+ if (this.filtered(this.eFam[j], this.eHunkFam[j])) {
1382
+ changed = false;
1383
+ ev.color = t.edge[F_NEUTRAL];
1384
+ ev.width = STROKE.neutralEdge;
1385
+ ev.arrowMinZoom = LOD.arrow;
1386
+ if (ev.label !== null)
1387
+ ev.label.color = t.neutralLabel;
1388
+ f *= EMPHASIS.filtered;
1389
+ }
1390
+ let level;
1391
+ if (this.layer === L_REPLAY) {
1392
+ level =
1393
+ this.edgeGen[j] === this.gen
1394
+ ? 2
1395
+ : this.nodeLevel(es) >= 1 && this.nodeLevel(et) >= 1
1396
+ ? 1
1397
+ : 0;
1398
+ }
1399
+ else {
1400
+ level =
1401
+ this.selHunk >= 0 && this.eHunk[j] === this.selHunk
1402
+ ? 2
1403
+ : this.selNode[es] >= 1 && this.selNode[et] >= 1
1404
+ ? 1
1405
+ : 0;
1406
+ }
1407
+ f *= this.spotlight(level);
1408
+ const selected = this.selHunk >= 0 && this.eHunk[j] === this.selHunk;
1409
+ const hovered = view.hoveredEdge === j;
1410
+ let minZoom = changed ? LOD.edgeLabel : 0;
1411
+ let priority = changed ? 0.9 : 0.4;
1412
+ if (selected) {
1413
+ if (ev.width < STROKE.selectedEdge)
1414
+ ev.width += (STROKE.selectedEdge - ev.width) * this.emph;
1415
+ ev.arrowMinZoom = 0;
1416
+ ev.z = Math.max(ev.z, 3);
1417
+ minZoom = 0;
1418
+ priority = 3;
1419
+ }
1420
+ if (hovered) {
1421
+ ev.width = Math.max(ev.width, STROKE.selectedEdge);
1422
+ ev.arrowMinZoom = 0;
1423
+ ev.z = 4;
1424
+ if (ev.label === null) {
1425
+ const label = pool.edgeLabels[j];
1426
+ label.text = this.eLabel[j];
1427
+ label.color = t.neutralLabel;
1428
+ label.alpha = 1;
1429
+ label.size = LABEL.edgeSize;
1430
+ label.weight = t.weightRegular;
1431
+ label.strike = false;
1432
+ label.offset = 0;
1433
+ ev.label = label;
1434
+ }
1435
+ minZoom = 0;
1436
+ priority = Math.max(priority, 2.5);
1437
+ }
1438
+ if (ev.label !== null && this.fadesRestLabels(level) && !hovered) {
1439
+ if (this.restLabels <= 0)
1440
+ ev.label = null;
1441
+ else
1442
+ ev.label.alpha *= this.restLabels;
1443
+ }
1444
+ ev.alpha *= f;
1445
+ if (ev.label !== null) {
1446
+ ev.label.minZoom = minZoom;
1447
+ ev.label.priority = priority;
1448
+ }
1449
+ }
1450
+ // ─── Glyphs ───────────────────────────────────────────────────────────────
1451
+ resolveGlyphs(frame, pool) {
1452
+ const out = frame.glyphs;
1453
+ const list = this.glyphLists[this.layer];
1454
+ let count = 0;
1455
+ for (let k = 0; k < list.length; k++) {
1456
+ const spec = this.glyphs[list[k]];
1457
+ const g = pool.glyphs[count];
1458
+ if (this.writeGlyph(spec, g, pool.glyphLabels[count], frame.nodes)) {
1459
+ if (out[count] !== g)
1460
+ out[count] = g;
1461
+ count++;
1462
+ }
1463
+ }
1464
+ if (out.length !== count)
1465
+ out.length = count;
1466
+ }
1467
+ /** Glyph line between node centers; renderers stop it at `r1` / `r2` like an edge. */
1468
+ glyphLine(g, spec) {
1469
+ const { nx, ny, nr } = this;
1470
+ g.x1 = nx[spec.a];
1471
+ g.y1 = ny[spec.a];
1472
+ g.x2 = nx[spec.b];
1473
+ g.y2 = ny[spec.b];
1474
+ g.r1 = nr[spec.a] + spec.clearA;
1475
+ g.r2 = nr[spec.b] + spec.clearB;
1476
+ }
1477
+ writeGlyph(spec, g, label, nodes) {
1478
+ const view = this.view;
1479
+ const focus = view.focus;
1480
+ if (focus !== null && (focus.nodes[spec.a] !== 1 || focus.nodes[spec.b] !== 1))
1481
+ return false;
1482
+ let alpha = spec.alpha;
1483
+ let progress = 1;
1484
+ let color = spec.color;
1485
+ let dash = spec.dash;
1486
+ let width = spec.width;
1487
+ if (this.layer === L_REPLAY) {
1488
+ const h = spec.hunk;
1489
+ if (h < 0)
1490
+ return false;
1491
+ const state = this.hState[h];
1492
+ const p = this.hP[h];
1493
+ const inTransform = state === 1 && p >= SPOT_END && p < SETTLE;
1494
+ const u = inTransform ? (p - SPOT_END) / TRANSFORM : 0;
1495
+ const c = this.c;
1496
+ switch (spec.role) {
1497
+ case G_TRAIL: {
1498
+ if (!inTransform || this.reduced)
1499
+ return false;
1500
+ const a = c.nA[spec.node];
1501
+ const b = c.nB[spec.node];
1502
+ progress = easeInOut(segment(u, a, b));
1503
+ alpha *= 0.6 * (1 - segment(u, b + 0.05, 1));
1504
+ if (progress <= 0 || alpha <= 0)
1505
+ return false;
1506
+ break;
1507
+ }
1508
+ case G_REPLACEMENT: {
1509
+ if (state === 0 || (state === 1 && p < SPOT_END))
1510
+ return false;
1511
+ if (inTransform) {
1512
+ if (this.reduced)
1513
+ alpha *= easeStandard(u);
1514
+ else
1515
+ progress = easeStandard(segment(u, 0.4, 0.9));
1516
+ if (progress <= 0 || alpha <= 0)
1517
+ return false;
1518
+ }
1519
+ break;
1520
+ }
1521
+ case G_GHOST: {
1522
+ if (!inTransform)
1523
+ return false;
1524
+ const kind = c.eKind[spec.edge];
1525
+ if (kind === EA_SWING && !this.reduced) {
1526
+ alpha *= 1 - easeStandard(segment(u, CHOREO.ghostFade[0], CHOREO.ghostFade[1]));
1527
+ }
1528
+ else if (kind === EA_SWAP || kind === EA_SWING || this.reduced) {
1529
+ // Crossfade: the old form in its before style fades out as the new one fades in.
1530
+ const before = this.eStyles[spec.edge][L_BEFORE];
1531
+ color = before.color;
1532
+ dash = before.dash;
1533
+ width = before.width;
1534
+ alpha =
1535
+ 1 -
1536
+ easeStandard(this.reduced ? u : segment(u, c.eA[spec.edge], c.eB[spec.edge]));
1537
+ }
1538
+ else
1539
+ return false;
1540
+ if (alpha <= 0)
1541
+ return false;
1542
+ break;
1543
+ }
1544
+ case G_PIVOT: {
1545
+ if (!inTransform || this.reduced || c.eKind[spec.edge] !== EA_SWING)
1546
+ return false;
1547
+ alpha *= 1 - segment(u, CHOREO.pivotFade[0], CHOREO.pivotFade[1]);
1548
+ if (alpha <= 0)
1549
+ return false;
1550
+ break;
1551
+ }
1552
+ default:
1553
+ return false;
1554
+ }
1555
+ }
1556
+ else if (spec.role === G_SUGGESTION && !view.showSuggestions)
1557
+ return false;
1558
+ else if (spec.edge >= 0 && nodes[spec.a]?.visible !== true)
1559
+ return false;
1560
+ g.kind = spec.kind;
1561
+ if (spec.role === G_PIVOT) {
1562
+ const { nx, ny, nr } = this;
1563
+ const px = nx[spec.a];
1564
+ const py = ny[spec.a];
1565
+ const pr = nr[spec.a];
1566
+ const dx = nx[spec.b] - px;
1567
+ const dy = ny[spec.b] - py;
1568
+ const len = Math.sqrt(dx * dx + dy * dy);
1569
+ g.x1 = len > 0 ? px + (dx / len) * pr : px;
1570
+ g.y1 = len > 0 ? py + (dy / len) * pr : py - pr;
1571
+ g.x2 = g.x1;
1572
+ g.y2 = g.y1;
1573
+ g.r1 = 0;
1574
+ g.r2 = 0;
1575
+ g.curvature = 0;
1576
+ }
1577
+ else {
1578
+ this.glyphLine(g, spec);
1579
+ g.curvature = spec.curvature;
1580
+ }
1581
+ // Modulators: filter, spotlight.
1582
+ let f = 1;
1583
+ if (this.filtered(spec.family, spec.hunk >= 0 ? this.hFam[spec.hunk] : F_NEUTRAL)) {
1584
+ color = this.t.edge[F_NEUTRAL];
1585
+ f *= EMPHASIS.filtered;
1586
+ }
1587
+ const level = Math.max(this.nodeLevel(spec.a), this.nodeLevel(spec.b));
1588
+ f *= this.spotlight(level);
1589
+ g.color = color;
1590
+ g.alpha = alpha * f;
1591
+ g.width = width;
1592
+ g.dash = dash;
1593
+ g.progress = progress;
1594
+ const fade = this.fadesRestLabels(level);
1595
+ if (spec.label !== null && !(fade && this.restLabels <= 0)) {
1596
+ label.text = spec.label;
1597
+ label.color = this.t.text[F_IDENTITY];
1598
+ label.alpha = fade ? this.restLabels : 1;
1599
+ label.size = LABEL.size;
1600
+ label.weight = this.t.weightStrong;
1601
+ label.strike = false;
1602
+ label.priority = 1;
1603
+ label.minZoom = this.lodChanged;
1604
+ label.offset = 0;
1605
+ g.label = label;
1606
+ }
1607
+ else
1608
+ g.label = null;
1609
+ return g.alpha > 0;
1610
+ }
1611
+ }
1612
+ /**
1613
+ * A resolver that turns a `ViewState` into drawable primitives for `scene`
1614
+ * (docs/visual-language.md). Construction is O(N + E); `resolve` is O(N + E) per frame and,
1615
+ * when `out` is passed back in, allocates no objects or arrays (see the module notes for the
1616
+ * transient number boxing V8 may still do while replay animations run).
1617
+ */
1618
+ export function createVisualResolver(scene) {
1619
+ return new SceneResolver(scene);
1620
+ }
1621
+ //# sourceMappingURL=resolver.js.map