@kgdiff/viewer 0.2.0 → 0.3.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.
- package/README.md +44 -3
- package/dist/controller.d.ts +40 -7
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +194 -45
- package/dist/controller.js.map +1 -1
- package/dist/element.d.ts +12 -2
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +132 -8
- package/dist/element.js.map +1 -1
- package/dist/framing.d.ts +20 -1
- package/dist/framing.d.ts.map +1 -1
- package/dist/framing.js +49 -0
- package/dist/framing.js.map +1 -1
- package/dist/index.d.ts +48 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/prefs.d.ts +7 -1
- package/dist/prefs.d.ts.map +1 -1
- package/dist/prefs.js +15 -0
- package/dist/prefs.js.map +1 -1
- package/dist/standalone/kgdiff-viewer.js +4 -4
- package/dist/styles.d.ts.map +1 -1
- package/dist/styles.js +17 -0
- package/dist/styles.js.map +1 -1
- package/dist/ui/stage.d.ts +4 -0
- package/dist/ui/stage.d.ts.map +1 -1
- package/dist/ui/stage.js +30 -4
- package/dist/ui/stage.js.map +1 -1
- package/package.json +6 -6
- package/src/controller.ts +206 -41
- package/src/element.ts +148 -8
- package/src/framing.ts +65 -1
- package/src/index.ts +47 -1
- package/src/prefs.ts +26 -1
- package/src/styles.ts +17 -0
- package/src/ui/stage.ts +44 -3
package/README.md
CHANGED
|
@@ -38,9 +38,50 @@ ESM only. Runs in evergreen browsers.
|
|
|
38
38
|
`@kgdiff/viewer/standalone/compute` add-on brings the diff engine and force layout for `load()`.
|
|
39
39
|
|
|
40
40
|
Attributes: `mode` (`overlay` | `swipe` | `replay` | `changes`), `theme`, `palette`, `locale`,
|
|
41
|
-
`sidebar`, `sync-hash`, `global-shortcuts
|
|
42
|
-
`
|
|
43
|
-
`
|
|
41
|
+
`sidebar`, `sync-hash`, `global-shortcuts`, `header`, `chrome`, `legend`, `loop`, `loop-delay`,
|
|
42
|
+
`pause-on-hover`. Methods: `load`, `selectHunk`, `next`, `prev`, `play`, `pause`, `fit`,
|
|
43
|
+
`exportDelta`, `reviewState`, `setReviewed`. Events: `kgdiff-ready`, `kgdiff-select`,
|
|
44
|
+
`kgdiff-mode`, `kgdiff-review`, `kgdiff-error`, `kgdiff-replay-end`.
|
|
45
|
+
|
|
46
|
+
## Embed a small looping replay
|
|
47
|
+
|
|
48
|
+
To show a graph's changes as a small strip on another page, leave the review UI out and let the
|
|
49
|
+
replay loop:
|
|
50
|
+
|
|
51
|
+
```html
|
|
52
|
+
<kgdiff-viewer mode="replay" chrome="none" loop style="height: 320px"></kgdiff-viewer>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| Attribute (property) | Values | Default |
|
|
56
|
+
| --------------------------------- | ------------------------------ | ------------------------------------------------ |
|
|
57
|
+
| `chrome` (`chrome`) | `full`, `none` | `full` |
|
|
58
|
+
| `legend` (`legend`) | `auto`, `full`, `mini`, `none` | `auto`: `full`, or `mini` with `chrome="none"` |
|
|
59
|
+
| `loop` (`loop`) | boolean | off |
|
|
60
|
+
| `loop-delay` (`loopDelay`) | ms between the end and restart | `1200` |
|
|
61
|
+
| `pause-on-hover` (`pauseOnHover`) | `false` to keep playing | on: a looping replay pauses under a mouse or pen |
|
|
62
|
+
|
|
63
|
+
- `chrome="none"` shows the graph alone: no header (view switch, theme, full screen, help,
|
|
64
|
+
sidebar button), change list, step caption, playback bar, finished card, zoom buttons, minimap
|
|
65
|
+
or legend card. The hover tooltip stays, and the legend becomes one line with the families this
|
|
66
|
+
diff has, then unchanged. Clicks and keyboard shortcuts do nothing. The minimum height drops
|
|
67
|
+
from 420px to 120px. The view stays fitted: when the canvas is resized (unless the user
|
|
68
|
+
panned or zoomed since) and at the start of every loop. Labels are never cut at the edge: one
|
|
69
|
+
pushed from its spot by a neighbour moves inside the canvas or is left out (the tooltip
|
|
70
|
+
still names it).
|
|
71
|
+
- `loop` plays the replay by itself (in `replay` mode) and starts it again `loop-delay` ms after
|
|
72
|
+
it ends. `pause()` stops it until `play()`; a new diff starts it again.
|
|
73
|
+
- `kgdiff-replay-end` fires each time playback reaches the end, with `detail.loop` telling
|
|
74
|
+
whether it will start again. Set `loop = false` or call `pause()` in the listener to stop
|
|
75
|
+
there.
|
|
76
|
+
- `fit({ padding })` fits the graph with its labels into view, keeping `padding` px around them
|
|
77
|
+
(default 24, kept for the later automatic fits); `fit({ animate: false })` jumps.
|
|
78
|
+
|
|
79
|
+
```js
|
|
80
|
+
let rounds = 0;
|
|
81
|
+
viewer.addEventListener('kgdiff-replay-end', (event) => {
|
|
82
|
+
if (event.detail.loop && ++rounds === 3) viewer.loop = false; // three rounds, then stop
|
|
83
|
+
});
|
|
84
|
+
```
|
|
44
85
|
|
|
45
86
|
## Learn more
|
|
46
87
|
|
package/dist/controller.d.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import type { DiffResult, DiffStats, Locale, Rect, UnionId } from '@kgdiff/core';
|
|
14
14
|
import type { LayoutResult } from '@kgdiff/layout';
|
|
15
|
-
import type
|
|
15
|
+
import { type Camera } from '@kgdiff/renderer';
|
|
16
16
|
import { type ChangeFamily, type FocusState, type PaletteName, type Scene, type Theme, type ThemeName, type VisualResolver } from '@kgdiff/scene';
|
|
17
17
|
import { type ChromeLayout } from './framing.js';
|
|
18
18
|
import type { LoadOptions, ReportPayload, ReviewState, ThemePreference, ViewerMode } from './index.js';
|
|
@@ -30,6 +30,13 @@ export interface ControllerHost {
|
|
|
30
30
|
review(hunk: string, reviewed: boolean, progress: ReviewState): void;
|
|
31
31
|
error(error: Error): void;
|
|
32
32
|
announce(text: string): void;
|
|
33
|
+
/** Playback reached the end of the replay; `loop`: it starts again after the loop delay. */
|
|
34
|
+
replayEnd(loop: boolean): void;
|
|
35
|
+
}
|
|
36
|
+
export interface FitOptions {
|
|
37
|
+
readonly animate?: boolean;
|
|
38
|
+
/** Room around the content, px (default `FRAME_PADDING`); kept for later automatic fits. */
|
|
39
|
+
readonly padding?: number;
|
|
33
40
|
}
|
|
34
41
|
export interface ControllerDeps {
|
|
35
42
|
readonly now: () => number;
|
|
@@ -84,6 +91,13 @@ export declare class ViewerController {
|
|
|
84
91
|
clock: ReplayClock | null;
|
|
85
92
|
split: number;
|
|
86
93
|
emphasis: number;
|
|
94
|
+
/**
|
|
95
|
+
* Keep the view fitted (embeds without controls): fit again when the canvas is resized unless
|
|
96
|
+
* the user moved the camera since, and at the start of every loop.
|
|
97
|
+
*/
|
|
98
|
+
autoFit: boolean;
|
|
99
|
+
/** Canvas clicks select (and double clicks fly); off for embeds without controls. */
|
|
100
|
+
selectOnClick: boolean;
|
|
87
101
|
constructor(host: ControllerHost, deps: ControllerDeps);
|
|
88
102
|
subscribe(listener: (topics: ReadonlySet<Topic>) => void): () => void;
|
|
89
103
|
/** Deliver pending notifications now (also used by tests). */
|
|
@@ -108,7 +122,15 @@ export declare class ViewerController {
|
|
|
108
122
|
* Every fit and flight frames its target in the free part of the canvas (`framing.ts`).
|
|
109
123
|
*/
|
|
110
124
|
setChrome(provider: (() => ChromeLayout) | null): void;
|
|
111
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Keep labels whole inside the canvas (embeds without camera controls): a label pushed from
|
|
127
|
+
* its spot by a neighbour moves inside or is left out, never cut at the edge.
|
|
128
|
+
*/
|
|
129
|
+
setLabelsInside(inside: boolean): void;
|
|
130
|
+
/**
|
|
131
|
+
* The canvas was laid out or resized: run the initial fit a new diff is waiting for, or (with
|
|
132
|
+
* `autoFit`) fit again unless the user moved the camera since the last fit.
|
|
133
|
+
*/
|
|
112
134
|
layoutChanged(): void;
|
|
113
135
|
/** The user panned / zoomed: suspend replay camera follow for `USER_CAMERA_GRACE`. */
|
|
114
136
|
noteUserCamera(): void;
|
|
@@ -143,12 +165,10 @@ export declare class ViewerController {
|
|
|
143
165
|
flyToNode(i: number): void;
|
|
144
166
|
flyToSelection(): void;
|
|
145
167
|
/**
|
|
146
|
-
* Fit everything visible (the focus in changes-only mode) into the free part
|
|
147
|
-
* Before the canvas is laid out it falls back to the renderer's own fit.
|
|
168
|
+
* Fit everything visible (the focus in changes-only mode), labels included, into the free part
|
|
169
|
+
* of the canvas. Before the canvas is laid out it falls back to the renderer's own fit.
|
|
148
170
|
*/
|
|
149
|
-
fit(options?:
|
|
150
|
-
readonly animate?: boolean;
|
|
151
|
-
}): Promise<void>;
|
|
171
|
+
fit(options?: FitOptions): Promise<void>;
|
|
152
172
|
zoom(direction: 1 | -1): void;
|
|
153
173
|
/** Clear hover state (pointer left the canvas). */
|
|
154
174
|
clearHover(): void;
|
|
@@ -178,8 +198,21 @@ export declare class ViewerController {
|
|
|
178
198
|
/** Bring a union node into the scene and the focus (search results), then select it. */
|
|
179
199
|
revealNode(id: UnionId): Promise<void>;
|
|
180
200
|
play(): void;
|
|
201
|
+
/** Pause; a looping replay stays paused (until `play`, a new diff or loop turned on again). */
|
|
181
202
|
pause(): void;
|
|
182
203
|
togglePlay(): void;
|
|
204
|
+
/**
|
|
205
|
+
* Play the replay over and over: `delay` ms after it ends it starts again; null plays it
|
|
206
|
+
* once. Turning the loop on starts a replay that is shown but not playing.
|
|
207
|
+
*/
|
|
208
|
+
setLoop(delay: number | null): void;
|
|
209
|
+
get loopDelay(): number | null;
|
|
210
|
+
/**
|
|
211
|
+
* Hold playback while the pointer rests on the graph (`pause-on-hover`): a playing replay
|
|
212
|
+
* pauses and continues on release; a pending loop restart waits for the release, then the
|
|
213
|
+
* full loop delay.
|
|
214
|
+
*/
|
|
215
|
+
hold(held: boolean): void;
|
|
183
216
|
seek(t: number): void;
|
|
184
217
|
stepReplay(direction: 1 | -1): void;
|
|
185
218
|
restart(): void;
|
package/dist/controller.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEjF,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACnD,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"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"}
|
package/dist/controller.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { KgdiffError } from '@kgdiff/core';
|
|
2
|
+
import { labelFootprint, } from '@kgdiff/renderer';
|
|
2
3
|
import { buildScene, computeFocus, createViewState, createVisualResolver, frameBounds, getTheme, } from '@kgdiff/scene';
|
|
3
4
|
import { computeNow, computeStatus, loadCompute } from './compute-loader.js';
|
|
4
|
-
import { chromeInsets, FRAME_PADDING, freeView, NO_INSETS, } from './framing.js';
|
|
5
|
+
import { chromeInsets, FRAME_PADDING, freeView, NO_INSETS, zoomFit, } from './framing.js';
|
|
5
6
|
import { translate } from './i18n.js';
|
|
6
7
|
import { buildList, EMPTY_LIST, stepHunk } from './list-model.js';
|
|
7
8
|
import { readPayload, runLoad } from './pipeline.js';
|
|
@@ -72,6 +73,14 @@ export class ViewerController {
|
|
|
72
73
|
clock = null;
|
|
73
74
|
split = 0.5;
|
|
74
75
|
emphasis = 1;
|
|
76
|
+
/**
|
|
77
|
+
* Keep the view fitted (embeds without controls): fit again when the canvas is resized unless
|
|
78
|
+
* the user moved the camera since, and at the start of every loop.
|
|
79
|
+
*/
|
|
80
|
+
autoFit = false;
|
|
81
|
+
/** Canvas clicks select (and double clicks fly); off for embeds without controls. */
|
|
82
|
+
selectOnClick = true;
|
|
83
|
+
#labelsInside = false;
|
|
75
84
|
#review = null;
|
|
76
85
|
#searchIndex = null;
|
|
77
86
|
#renderer = null;
|
|
@@ -94,6 +103,16 @@ export class ViewerController {
|
|
|
94
103
|
#savedCamera = null;
|
|
95
104
|
/** A new diff is installed but has not been fitted to a laid-out canvas yet. */
|
|
96
105
|
#needsFit = false;
|
|
106
|
+
/** The user panned or zoomed since the last fit. */
|
|
107
|
+
#userMoved = false;
|
|
108
|
+
#fitPadding = FRAME_PADDING;
|
|
109
|
+
/** A finished replay starts again this long (ms) after its end; null: it plays once. */
|
|
110
|
+
#loopDelay = null;
|
|
111
|
+
/** When a finished looping replay starts again (`deps.now()` time), else NaN. */
|
|
112
|
+
#restartAt = Number.NaN;
|
|
113
|
+
/** Playback is held (pointer over the graph); `#resume`: it was playing and continues after. */
|
|
114
|
+
#held = false;
|
|
115
|
+
#resume = false;
|
|
97
116
|
#listeners = new Set();
|
|
98
117
|
#pendingTopics = new Set();
|
|
99
118
|
#flushQueued = false;
|
|
@@ -206,6 +225,8 @@ export class ViewerController {
|
|
|
206
225
|
fail(error) {
|
|
207
226
|
this.#loadToken++;
|
|
208
227
|
this.clock?.pause();
|
|
228
|
+
this.#restartAt = Number.NaN;
|
|
229
|
+
this.#resume = false;
|
|
209
230
|
this.status = 'error';
|
|
210
231
|
this.error = error;
|
|
211
232
|
this.step = null;
|
|
@@ -275,6 +296,8 @@ export class ViewerController {
|
|
|
275
296
|
this.#pendingHunk = null;
|
|
276
297
|
if (pending !== null)
|
|
277
298
|
this.selectHunkById(pending.id, { fly: pending.fly });
|
|
299
|
+
else
|
|
300
|
+
this.#autoplay();
|
|
278
301
|
}
|
|
279
302
|
/** (Re)build scene, resolver, list, clock and focus; selection is carried over by id. */
|
|
280
303
|
#rebuildScene() {
|
|
@@ -328,7 +351,10 @@ export class ViewerController {
|
|
|
328
351
|
if (this.#renderer)
|
|
329
352
|
return;
|
|
330
353
|
const backend = renderBackend();
|
|
331
|
-
const renderer = backend.createRenderer(stage, {
|
|
354
|
+
const renderer = backend.createRenderer(stage, {
|
|
355
|
+
theme: this.theme,
|
|
356
|
+
labelsInside: this.#labelsInside,
|
|
357
|
+
});
|
|
332
358
|
this.#renderer = renderer;
|
|
333
359
|
// Re-attached (element moved): keep the view; this also stops the renderer's auto-fit.
|
|
334
360
|
if (this.#savedCamera)
|
|
@@ -371,10 +397,26 @@ export class ViewerController {
|
|
|
371
397
|
setChrome(provider) {
|
|
372
398
|
this.#chrome = provider;
|
|
373
399
|
}
|
|
374
|
-
/**
|
|
400
|
+
/**
|
|
401
|
+
* Keep labels whole inside the canvas (embeds without camera controls): a label pushed from
|
|
402
|
+
* its spot by a neighbour moves inside or is left out, never cut at the edge.
|
|
403
|
+
*/
|
|
404
|
+
setLabelsInside(inside) {
|
|
405
|
+
this.#labelsInside = inside;
|
|
406
|
+
this.#renderer?.setLabelsInside(inside);
|
|
407
|
+
}
|
|
408
|
+
/**
|
|
409
|
+
* The canvas was laid out or resized: run the initial fit a new diff is waiting for, or (with
|
|
410
|
+
* `autoFit`) fit again unless the user moved the camera since the last fit.
|
|
411
|
+
*/
|
|
375
412
|
layoutChanged() {
|
|
376
|
-
if (
|
|
413
|
+
if (this.status !== 'ready')
|
|
414
|
+
return;
|
|
415
|
+
if (!this.#needsFit) {
|
|
416
|
+
if (this.autoFit && !this.#userMoved)
|
|
417
|
+
void this.fit({ animate: false });
|
|
377
418
|
return;
|
|
419
|
+
}
|
|
378
420
|
// A selection made before the first layout (deep link, `kgdiff-ready` handler) wins.
|
|
379
421
|
const target = this.#selectionBounds();
|
|
380
422
|
if (target) {
|
|
@@ -384,17 +426,16 @@ export class ViewerController {
|
|
|
384
426
|
this.#flyTo(target, { animate: false });
|
|
385
427
|
return;
|
|
386
428
|
}
|
|
387
|
-
const
|
|
388
|
-
const padding = bounds ? this.#frameInsets(bounds, FIT_MAX_ZOOM) : null;
|
|
429
|
+
const fit = this.#fitTarget();
|
|
389
430
|
const camera = this.#renderer?.camera;
|
|
390
|
-
if (!
|
|
431
|
+
if (!fit || !camera)
|
|
391
432
|
return;
|
|
392
|
-
this.#
|
|
393
|
-
void camera.fit(bounds, { animate: false, padding, maxZoom: FIT_MAX_ZOOM });
|
|
433
|
+
void this.#fitTo(camera, fit, { animate: false });
|
|
394
434
|
}
|
|
395
435
|
/** The user panned / zoomed: suspend replay camera follow for `USER_CAMERA_GRACE`. */
|
|
396
436
|
noteUserCamera() {
|
|
397
437
|
this.#lastUserCamera = this.#deps.now();
|
|
438
|
+
this.#userMoved = true;
|
|
398
439
|
}
|
|
399
440
|
// ─── Frame loop ───────────────────────────────────────────────────────────
|
|
400
441
|
/** Redraw on the next animation frame. */
|
|
@@ -423,7 +464,13 @@ export class ViewerController {
|
|
|
423
464
|
if (clock.playing)
|
|
424
465
|
animating = true;
|
|
425
466
|
else if (clock.finished)
|
|
426
|
-
this.#
|
|
467
|
+
this.#ended();
|
|
468
|
+
}
|
|
469
|
+
if (!Number.isNaN(this.#restartAt) && !this.#held) {
|
|
470
|
+
if (now >= this.#restartAt)
|
|
471
|
+
this.#loopAgain();
|
|
472
|
+
else
|
|
473
|
+
animating = true;
|
|
427
474
|
}
|
|
428
475
|
if (this.#emphasisAnim) {
|
|
429
476
|
const a = this.#emphasisAnim;
|
|
@@ -529,8 +576,11 @@ export class ViewerController {
|
|
|
529
576
|
if (mode === null || mode === this.mode)
|
|
530
577
|
return;
|
|
531
578
|
this.mode = mode;
|
|
532
|
-
if (mode !== 'replay')
|
|
579
|
+
if (mode !== 'replay') {
|
|
533
580
|
this.clock?.pause();
|
|
581
|
+
this.#restartAt = Number.NaN;
|
|
582
|
+
this.#resume = false;
|
|
583
|
+
}
|
|
534
584
|
this.#followed = '';
|
|
535
585
|
this.#updateFocus();
|
|
536
586
|
this.#minimapDirty = true;
|
|
@@ -540,6 +590,8 @@ export class ViewerController {
|
|
|
540
590
|
this.#host.mode(mode);
|
|
541
591
|
this.#host.announce(translate(this.locale, 'announce.mode', { mode: translate(this.locale, `mode.${mode}`) }));
|
|
542
592
|
}
|
|
593
|
+
if (mode === 'replay')
|
|
594
|
+
this.#autoplay();
|
|
543
595
|
}
|
|
544
596
|
// ─── Selection ────────────────────────────────────────────────────────────
|
|
545
597
|
selectHunkById(id, options = {}) {
|
|
@@ -581,6 +633,8 @@ export class ViewerController {
|
|
|
581
633
|
if (this.mode === 'replay' && this.clock) {
|
|
582
634
|
const end = this.clock.windowEnd(h);
|
|
583
635
|
this.clock.pause();
|
|
636
|
+
this.#restartAt = Number.NaN;
|
|
637
|
+
this.#resume = false;
|
|
584
638
|
if (end !== null)
|
|
585
639
|
this.clock.seek(end);
|
|
586
640
|
this.#followed = '';
|
|
@@ -795,47 +849,61 @@ export class ViewerController {
|
|
|
795
849
|
this.#flyTo(b);
|
|
796
850
|
}
|
|
797
851
|
/**
|
|
798
|
-
* Fit everything visible (the focus in changes-only mode) into the free part
|
|
799
|
-
* Before the canvas is laid out it falls back to the renderer's own fit.
|
|
852
|
+
* Fit everything visible (the focus in changes-only mode), labels included, into the free part
|
|
853
|
+
* of the canvas. Before the canvas is laid out it falls back to the renderer's own fit.
|
|
800
854
|
*/
|
|
801
855
|
fit(options = {}) {
|
|
802
856
|
const camera = this.#renderer?.camera;
|
|
803
857
|
if (!camera)
|
|
804
858
|
return Promise.resolve();
|
|
805
|
-
const
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
859
|
+
const { padding, ...transition } = options;
|
|
860
|
+
if (padding !== undefined) {
|
|
861
|
+
this.#fitPadding = Number.isFinite(padding) && padding >= 0 ? padding : FRAME_PADDING;
|
|
862
|
+
}
|
|
863
|
+
const fit = this.#fitTarget();
|
|
864
|
+
if (!fit)
|
|
865
|
+
return camera.fit(undefined, transition);
|
|
866
|
+
return this.#fitTo(camera, fit, transition);
|
|
867
|
+
}
|
|
868
|
+
#fitTo(camera, fit, transition) {
|
|
809
869
|
this.#needsFit = false;
|
|
810
|
-
|
|
870
|
+
this.#userMoved = false;
|
|
871
|
+
return camera.fit(fit.bounds, { ...transition, padding: fit.insets, maxZoom: FIT_MAX_ZOOM });
|
|
811
872
|
}
|
|
812
|
-
/**
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
873
|
+
/**
|
|
874
|
+
* Bounds and insets that show everything visible (the changes-only focus) with every label
|
|
875
|
+
* in its spot below its node, clear of the chrome, or null before layout. Measured in the
|
|
876
|
+
* union layer, which holds every state a replay or swipe passes through. Insets depend on
|
|
877
|
+
* what is framed, which depends on the zoom they allow: they are settled once more.
|
|
878
|
+
*/
|
|
879
|
+
#fitTarget() {
|
|
880
|
+
const layout = this.#chrome?.();
|
|
881
|
+
const resolver = this.resolver;
|
|
882
|
+
if (!layout || !resolver || !(layout.width > 0) || !(layout.height > 0))
|
|
816
883
|
return null;
|
|
817
|
-
const
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
884
|
+
const frame = resolver.resolve(createViewState({
|
|
885
|
+
layer: 'union',
|
|
886
|
+
focus: this.focus,
|
|
887
|
+
families: this.families,
|
|
888
|
+
reducedMotion: this.reducedMotion,
|
|
889
|
+
}));
|
|
890
|
+
const footprint = labelFootprint(frame, this.theme);
|
|
891
|
+
const extent = (k) => footprint.at(k);
|
|
892
|
+
const discs = extent(Number.POSITIVE_INFINITY);
|
|
893
|
+
if (!discs)
|
|
894
|
+
return null;
|
|
895
|
+
const { width, height } = layout;
|
|
896
|
+
const padding = this.#fitPadding;
|
|
897
|
+
let insets = chromeInsets(discs, layout, padding, FIT_MAX_ZOOM);
|
|
898
|
+
let fit = zoomFit(extent, width, height, insets, FIT_MAX_ZOOM);
|
|
899
|
+
if (!fit)
|
|
900
|
+
return null;
|
|
901
|
+
const settled = chromeInsets(fit.content, layout, padding, FIT_MAX_ZOOM);
|
|
902
|
+
if (!sameInsets(settled, insets)) {
|
|
903
|
+
insets = settled;
|
|
904
|
+
fit = zoomFit(extent, width, height, insets, FIT_MAX_ZOOM) ?? fit;
|
|
905
|
+
}
|
|
906
|
+
return { bounds: fit.bounds, insets };
|
|
839
907
|
}
|
|
840
908
|
zoom(direction) {
|
|
841
909
|
this.noteUserCamera();
|
|
@@ -878,6 +946,8 @@ export class ViewerController {
|
|
|
878
946
|
this.#onHover(null);
|
|
879
947
|
}
|
|
880
948
|
#onCanvasClick(pick) {
|
|
949
|
+
if (!this.selectOnClick)
|
|
950
|
+
return;
|
|
881
951
|
if (!pick) {
|
|
882
952
|
this.clearSelection();
|
|
883
953
|
return;
|
|
@@ -902,7 +972,7 @@ export class ViewerController {
|
|
|
902
972
|
}
|
|
903
973
|
}
|
|
904
974
|
#onCanvasDblClick(pick) {
|
|
905
|
-
if (pick?.type !== 'node')
|
|
975
|
+
if (!this.selectOnClick || pick?.type !== 'node')
|
|
906
976
|
return;
|
|
907
977
|
const n = this.scene?.nodes[pick.index];
|
|
908
978
|
if (!n)
|
|
@@ -1101,13 +1171,18 @@ export class ViewerController {
|
|
|
1101
1171
|
return;
|
|
1102
1172
|
if (this.mode !== 'replay')
|
|
1103
1173
|
this.setMode('replay');
|
|
1174
|
+
this.#restartAt = Number.NaN;
|
|
1175
|
+
this.#resume = false;
|
|
1104
1176
|
clock.play();
|
|
1105
1177
|
this.#followed = '';
|
|
1106
1178
|
this.#lastTick = 0;
|
|
1107
1179
|
this.#notify('replay');
|
|
1108
1180
|
this.invalidate();
|
|
1109
1181
|
}
|
|
1182
|
+
/** Pause; a looping replay stays paused (until `play`, a new diff or loop turned on again). */
|
|
1110
1183
|
pause() {
|
|
1184
|
+
this.#restartAt = Number.NaN;
|
|
1185
|
+
this.#resume = false;
|
|
1111
1186
|
if (!this.clock?.playing)
|
|
1112
1187
|
return;
|
|
1113
1188
|
this.clock.pause();
|
|
@@ -1120,10 +1195,79 @@ export class ViewerController {
|
|
|
1120
1195
|
else
|
|
1121
1196
|
this.play();
|
|
1122
1197
|
}
|
|
1198
|
+
/**
|
|
1199
|
+
* Play the replay over and over: `delay` ms after it ends it starts again; null plays it
|
|
1200
|
+
* once. Turning the loop on starts a replay that is shown but not playing.
|
|
1201
|
+
*/
|
|
1202
|
+
setLoop(delay) {
|
|
1203
|
+
const was = this.#loopDelay;
|
|
1204
|
+
this.#loopDelay = delay === null ? null : Number.isFinite(delay) ? Math.max(0, delay) : 0;
|
|
1205
|
+
if (this.#loopDelay === null)
|
|
1206
|
+
this.#restartAt = Number.NaN;
|
|
1207
|
+
else if (was === null)
|
|
1208
|
+
this.#autoplay();
|
|
1209
|
+
}
|
|
1210
|
+
get loopDelay() {
|
|
1211
|
+
return this.#loopDelay;
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* Hold playback while the pointer rests on the graph (`pause-on-hover`): a playing replay
|
|
1215
|
+
* pauses and continues on release; a pending loop restart waits for the release, then the
|
|
1216
|
+
* full loop delay.
|
|
1217
|
+
*/
|
|
1218
|
+
hold(held) {
|
|
1219
|
+
if (held === this.#held)
|
|
1220
|
+
return;
|
|
1221
|
+
this.#held = held;
|
|
1222
|
+
const clock = this.clock;
|
|
1223
|
+
if (held) {
|
|
1224
|
+
if (!clock?.playing)
|
|
1225
|
+
return;
|
|
1226
|
+
clock.pause();
|
|
1227
|
+
this.#resume = true;
|
|
1228
|
+
this.#notify('replay');
|
|
1229
|
+
this.invalidate();
|
|
1230
|
+
return;
|
|
1231
|
+
}
|
|
1232
|
+
if (!Number.isNaN(this.#restartAt) && this.#loopDelay !== null) {
|
|
1233
|
+
this.#restartAt = this.#deps.now() + this.#loopDelay;
|
|
1234
|
+
this.#schedule();
|
|
1235
|
+
}
|
|
1236
|
+
if (this.#resume)
|
|
1237
|
+
this.play();
|
|
1238
|
+
}
|
|
1239
|
+
/** Looping and showing a replay that is not playing: start it (or after the hold). */
|
|
1240
|
+
#autoplay() {
|
|
1241
|
+
if (this.#loopDelay === null || this.status !== 'ready' || this.mode !== 'replay')
|
|
1242
|
+
return;
|
|
1243
|
+
if (!this.clock || this.clock.playing || !Number.isNaN(this.#restartAt))
|
|
1244
|
+
return;
|
|
1245
|
+
if (this.#held)
|
|
1246
|
+
this.#resume = true;
|
|
1247
|
+
else
|
|
1248
|
+
this.play();
|
|
1249
|
+
}
|
|
1250
|
+
/** Playback reached the end: announce it, tell the host and arm the loop restart. */
|
|
1251
|
+
#ended() {
|
|
1252
|
+
this.#host.announce(translate(this.locale, 'announce.finished'));
|
|
1253
|
+
const loop = this.#loopDelay !== null;
|
|
1254
|
+
// Armed first, so a `kgdiff-replay-end` listener can still cancel it (pause, loop off).
|
|
1255
|
+
if (loop)
|
|
1256
|
+
this.#restartAt = this.#deps.now() + (this.#loopDelay ?? 0);
|
|
1257
|
+
this.#host.replayEnd(loop);
|
|
1258
|
+
}
|
|
1259
|
+
#loopAgain() {
|
|
1260
|
+
this.#restartAt = Number.NaN;
|
|
1261
|
+
if (this.autoFit)
|
|
1262
|
+
void this.fit();
|
|
1263
|
+
this.clock?.seek(0);
|
|
1264
|
+
this.play();
|
|
1265
|
+
}
|
|
1123
1266
|
seek(t) {
|
|
1124
1267
|
const clock = this.clock;
|
|
1125
1268
|
if (!clock)
|
|
1126
1269
|
return;
|
|
1270
|
+
this.#restartAt = Number.NaN;
|
|
1127
1271
|
clock.seek(t);
|
|
1128
1272
|
this.#notify('replay');
|
|
1129
1273
|
this.#follow();
|
|
@@ -1134,6 +1278,8 @@ export class ViewerController {
|
|
|
1134
1278
|
if (!clock)
|
|
1135
1279
|
return;
|
|
1136
1280
|
clock.step(direction);
|
|
1281
|
+
this.#restartAt = Number.NaN;
|
|
1282
|
+
this.#resume = false;
|
|
1137
1283
|
this.#followed = '';
|
|
1138
1284
|
// Follow (and announce) the hunk that just completed: stops sit at window ends.
|
|
1139
1285
|
const w = clock.timeline.windows[clock.position() - 1];
|
|
@@ -1245,6 +1391,9 @@ export class ViewerController {
|
|
|
1245
1391
|
function isString(x) {
|
|
1246
1392
|
return x !== undefined;
|
|
1247
1393
|
}
|
|
1394
|
+
function sameInsets(a, b) {
|
|
1395
|
+
return a.top === b.top && a.right === b.right && a.bottom === b.bottom && a.left === b.left;
|
|
1396
|
+
}
|
|
1248
1397
|
/** Pad a small rect so a node is shown with its neighbourhood. */
|
|
1249
1398
|
function padRect(r, min = 200, ratio = 1.4) {
|
|
1250
1399
|
const w = Math.max(min, r.width * ratio);
|