@kgdiff/viewer 0.3.0 → 0.4.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 +9 -8
- package/dist/controller.d.ts +23 -7
- package/dist/controller.d.ts.map +1 -1
- package/dist/controller.js +119 -14
- package/dist/controller.js.map +1 -1
- package/dist/element.d.ts +6 -3
- package/dist/element.d.ts.map +1 -1
- package/dist/element.js +68 -19
- package/dist/element.js.map +1 -1
- package/dist/hash-sync.d.ts +4 -2
- package/dist/hash-sync.d.ts.map +1 -1
- package/dist/hash-sync.js +21 -5
- package/dist/hash-sync.js.map +1 -1
- package/dist/i18n.d.ts +7 -7
- package/dist/i18n.d.ts.map +1 -1
- package/dist/i18n.js +14 -14
- package/dist/i18n.js.map +1 -1
- package/dist/index.d.ts +21 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/keyboard.d.ts +6 -4
- package/dist/keyboard.d.ts.map +1 -1
- package/dist/keyboard.js +6 -5
- package/dist/keyboard.js.map +1 -1
- package/dist/prefs.d.ts +7 -1
- package/dist/prefs.d.ts.map +1 -1
- package/dist/prefs.js +15 -3
- 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 +69 -63
- package/dist/styles.js.map +1 -1
- package/dist/ui/context.d.ts +2 -0
- package/dist/ui/context.d.ts.map +1 -1
- package/dist/ui/context.js.map +1 -1
- package/dist/ui/dock.d.ts +17 -0
- package/dist/ui/dock.d.ts.map +1 -0
- package/dist/ui/dock.js +151 -0
- package/dist/ui/dock.js.map +1 -0
- package/dist/ui/header.d.ts +7 -2
- package/dist/ui/header.d.ts.map +1 -1
- package/dist/ui/header.js +24 -133
- package/dist/ui/header.js.map +1 -1
- package/dist/ui/popovers.d.ts.map +1 -1
- package/dist/ui/popovers.js +3 -2
- package/dist/ui/popovers.js.map +1 -1
- package/dist/ui/sidebar.d.ts.map +1 -1
- package/dist/ui/sidebar.js +2 -1
- package/dist/ui/sidebar.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 +41 -94
- package/dist/ui/stage.js.map +1 -1
- package/dist/ui/swatch.d.ts +7 -0
- package/dist/ui/swatch.d.ts.map +1 -0
- package/dist/ui/swatch.js +26 -0
- package/dist/ui/swatch.js.map +1 -0
- package/package.json +6 -6
- package/src/controller.ts +131 -20
- package/src/element.ts +72 -21
- package/src/hash-sync.ts +20 -6
- package/src/i18n.ts +15 -14
- package/src/index.ts +21 -9
- package/src/keyboard.ts +12 -6
- package/src/prefs.ts +19 -4
- package/src/styles.ts +69 -63
- package/src/ui/context.ts +2 -0
- package/src/ui/dock.ts +198 -0
- package/src/ui/header.ts +38 -168
- package/src/ui/popovers.ts +3 -2
- package/src/ui/sidebar.ts +2 -1
- package/src/ui/stage.ts +49 -114
- package/src/ui/swatch.ts +31 -0
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @kgdiff/viewer
|
|
2
2
|
|
|
3
|
-
`<kgdiff-viewer>`, a framework-agnostic web component for reviewing graph diffs.
|
|
4
|
-
(Overlay, Swipe, Replay
|
|
3
|
+
`<kgdiff-viewer>`, a framework-agnostic web component for reviewing graph diffs. Three ways to
|
|
4
|
+
compare (Overlay, Swipe, Replay), each with or without Changes only, a searchable change list, a detail panel with
|
|
5
5
|
character-level diffs, review progress, deep links, keyboard shortcuts, light and dark themes, and
|
|
6
6
|
an English and Chinese UI.
|
|
7
7
|
|
|
@@ -37,9 +37,10 @@ ESM only. Runs in evergreen browsers.
|
|
|
37
37
|
self-contained script that defines the element and shows embedded payloads. The optional
|
|
38
38
|
`@kgdiff/viewer/standalone/compute` add-on brings the diff engine and force layout for `load()`.
|
|
39
39
|
|
|
40
|
-
Attributes: `mode` (`overlay` | `swipe` | `replay` | `changes`), `theme`,
|
|
41
|
-
`sidebar`, `sync-hash`, `global-shortcuts`, `header`, `chrome`, `legend`,
|
|
42
|
-
`pause-on-hover`.
|
|
40
|
+
Attributes: `mode` (`overlay` | `swipe` | `replay`), `scope` (`all` | `changes`), `theme`,
|
|
41
|
+
`palette`, `locale`, `sidebar`, `sync-hash`, `global-shortcuts`, `header`, `chrome`, `legend`,
|
|
42
|
+
`loop`, `loop-delay`, `pause-on-hover`. The legacy `mode="changes"` still works: it means
|
|
43
|
+
`overlay` with `scope="changes"`. Methods: `load`, `selectHunk`, `next`, `prev`, `play`, `pause`, `fit`,
|
|
43
44
|
`exportDelta`, `reviewState`, `setReviewed`. Events: `kgdiff-ready`, `kgdiff-select`,
|
|
44
45
|
`kgdiff-mode`, `kgdiff-review`, `kgdiff-error`, `kgdiff-replay-end`.
|
|
45
46
|
|
|
@@ -60,9 +61,9 @@ replay loop:
|
|
|
60
61
|
| `loop-delay` (`loopDelay`) | ms between the end and restart | `1200` |
|
|
61
62
|
| `pause-on-hover` (`pauseOnHover`) | `false` to keep playing | on: a looping replay pauses under a mouse or pen |
|
|
62
63
|
|
|
63
|
-
- `chrome="none"` shows the graph alone: no
|
|
64
|
-
|
|
65
|
-
or legend card. The hover tooltip stays, and the legend becomes one line with the families this
|
|
64
|
+
- `chrome="none"` shows the graph alone: no title, actions (theme, help, sidebar button), view
|
|
65
|
+
dock (with its step caption, playback bar and finished card), change list, zoom buttons,
|
|
66
|
+
minimap or legend card. The hover tooltip stays, and the legend becomes one line with the families this
|
|
66
67
|
diff has, then unchanged. Clicks and keyboard shortcuts do nothing. The minimum height drops
|
|
67
68
|
from 420px to 120px. The view stays fitted: when the canvas is resized (unless the user
|
|
68
69
|
panned or zoomed since) and at the start of every loop. Labels are never cut at the edge: one
|
package/dist/controller.d.ts
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* Viewer state and orchestration, independent of the UI:
|
|
3
3
|
*
|
|
4
4
|
* - load pipeline with progress steps (sources or a precomputed report);
|
|
5
|
-
* - mode, selection (with an animated spotlight `emphasis`), family filters,
|
|
6
|
-
*
|
|
5
|
+
* - view (mode × scope), selection (with an animated spotlight `emphasis`), family filters,
|
|
6
|
+
* review progress;
|
|
7
|
+
* - the `changes` scope's focus with expandable collapsed groups (`extendLayout` + scene rebuild);
|
|
7
8
|
* - replay clock driven by one requestAnimationFrame loop that sleeps when idle, with a camera
|
|
8
9
|
* that follows the playing hunk / chapter unless the user moved it in the last 2 s;
|
|
9
10
|
* - swipe position, hover, theme / palette / locale (rebuilding the scene when they change).
|
|
@@ -15,18 +16,19 @@ import type { LayoutResult } from '@kgdiff/layout';
|
|
|
15
16
|
import { type Camera } from '@kgdiff/renderer';
|
|
16
17
|
import { type ChangeFamily, type FocusState, type PaletteName, type Scene, type Theme, type ThemeName, type VisualResolver } from '@kgdiff/scene';
|
|
17
18
|
import { type ChromeLayout } from './framing.js';
|
|
18
|
-
import type { LoadOptions, ReportPayload, ReviewState, ThemePreference, ViewerMode } from './index.js';
|
|
19
|
+
import type { LoadOptions, ReportPayload, ReviewState, ThemePreference, ViewerMode, ViewerScope } from './index.js';
|
|
19
20
|
import { type ListModel } from './list-model.js';
|
|
20
21
|
import { type LoadedDiff, type LoadStep } from './pipeline.js';
|
|
22
|
+
import { LEGACY_CHANGES_MODE } from './prefs.js';
|
|
21
23
|
import { ReplayClock } from './replay.js';
|
|
22
24
|
import { type StorageLike } from './review-store.js';
|
|
23
25
|
import { type SearchHit } from './search.js';
|
|
24
26
|
export type Status = 'idle' | 'loading' | 'ready' | 'error';
|
|
25
|
-
export type Topic = 'status' | 'data' | 'mode' | 'selection' | 'filters' | 'review' | 'replay' | 'hover' | 'theme' | 'locale' | 'focus' | 'swipe';
|
|
27
|
+
export type Topic = 'status' | 'data' | 'mode' | 'selection' | 'filters' | 'review' | 'replay' | 'hover' | 'theme' | 'locale' | 'focus' | 'swipe' | 'overview';
|
|
26
28
|
export interface ControllerHost {
|
|
27
29
|
ready(stats: DiffStats): void;
|
|
28
30
|
select(hunk: string | null, node: string | null): void;
|
|
29
|
-
mode(mode: ViewerMode): void;
|
|
31
|
+
mode(mode: ViewerMode, scope: ViewerScope): void;
|
|
30
32
|
review(hunk: string, reviewed: boolean, progress: ReviewState): void;
|
|
31
33
|
error(error: Error): void;
|
|
32
34
|
announce(text: string): void;
|
|
@@ -46,7 +48,7 @@ export interface ControllerDeps {
|
|
|
46
48
|
readonly yieldToPaint: () => Promise<void>;
|
|
47
49
|
readonly storage: StorageLike | null;
|
|
48
50
|
}
|
|
49
|
-
export type Preference = 'mode' | 'theme' | 'palette' | 'locale';
|
|
51
|
+
export type Preference = 'mode' | 'scope' | 'theme' | 'palette' | 'locale';
|
|
50
52
|
/** Camera follow is suppressed this long after the user moved the camera (ms). */
|
|
51
53
|
export declare const USER_CAMERA_GRACE = 2000;
|
|
52
54
|
/** The camera follows when less than this share of the target bounds is in view. */
|
|
@@ -55,6 +57,8 @@ export declare const FOLLOW_COVERAGE = 0.7;
|
|
|
55
57
|
export declare const FLY_MAX_ZOOM = 1.8;
|
|
56
58
|
/** Fitting everything never zooms in further (the renderer's default). */
|
|
57
59
|
export declare const FIT_MAX_ZOOM = 2.5;
|
|
60
|
+
/** Everything counts as in view (the minimap hides) from this share of the content bounds on. */
|
|
61
|
+
export declare const OVERVIEW_COVERAGE = 0.98;
|
|
58
62
|
/** Share of `bounds` inside `view`, in [0, 1]. */
|
|
59
63
|
export declare function coverage(bounds: Rect, view: Rect): number;
|
|
60
64
|
export declare class ViewerController {
|
|
@@ -70,6 +74,7 @@ export declare class ViewerController {
|
|
|
70
74
|
resolver: VisualResolver | null;
|
|
71
75
|
list: ListModel;
|
|
72
76
|
mode: ViewerMode;
|
|
77
|
+
scope: ViewerScope;
|
|
73
78
|
themePreference: ThemePreference;
|
|
74
79
|
palette: PaletteName;
|
|
75
80
|
locale: Locale;
|
|
@@ -98,6 +103,8 @@ export declare class ViewerController {
|
|
|
98
103
|
autoFit: boolean;
|
|
99
104
|
/** Canvas clicks select (and double clicks fly); off for embeds without controls. */
|
|
100
105
|
selectOnClick: boolean;
|
|
106
|
+
/** The whole content is in view (the minimap has nothing to add). */
|
|
107
|
+
overview: boolean;
|
|
101
108
|
constructor(host: ControllerHost, deps: ControllerDeps);
|
|
102
109
|
subscribe(listener: (topics: ReadonlySet<Topic>) => void): () => void;
|
|
103
110
|
/** Deliver pending notifications now (also used by tests). */
|
|
@@ -138,7 +145,16 @@ export declare class ViewerController {
|
|
|
138
145
|
invalidate(): void;
|
|
139
146
|
/** Resolve the current view and hand it to the renderer (and the minimap). */
|
|
140
147
|
draw(now?: number): void;
|
|
141
|
-
|
|
148
|
+
/** Whether the scope is fixed to `changes` (the layout covers only the changes). */
|
|
149
|
+
get scopeLocked(): boolean;
|
|
150
|
+
/** Switch how the versions are compared. The legacy `changes` means overlay + `changes` scope. */
|
|
151
|
+
setMode(value: ViewerMode | typeof LEGACY_CHANGES_MODE): void;
|
|
152
|
+
/** Show the whole graph or only the changes, in any mode. */
|
|
153
|
+
setScope(value: ViewerScope): void;
|
|
154
|
+
/** Toggle the `changes` scope (key `4`); says why when the scope is locked. */
|
|
155
|
+
toggleScope(): void;
|
|
156
|
+
/** Set mode and scope together (one notification, one `kgdiff-mode` event). */
|
|
157
|
+
setView(mode: ViewerMode, scope: ViewerScope): void;
|
|
142
158
|
selectHunkById(id: string | null, options?: {
|
|
143
159
|
fly?: boolean;
|
|
144
160
|
}): 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
|
|
1
|
+
{"version":3,"file":"controller.d.ts","sourceRoot":"","sources":["../src/controller.ts"],"names":[],"mappings":"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"}
|
package/dist/controller.js
CHANGED
|
@@ -6,7 +6,7 @@ import { chromeInsets, FRAME_PADDING, freeView, NO_INSETS, zoomFit, } from './fr
|
|
|
6
6
|
import { translate } from './i18n.js';
|
|
7
7
|
import { buildList, EMPTY_LIST, stepHunk } from './list-model.js';
|
|
8
8
|
import { readPayload, runLoad } from './pipeline.js';
|
|
9
|
-
import { toLocale, toMode, toPalette, toTheme } from './prefs.js';
|
|
9
|
+
import { LEGACY_CHANGES_MODE, toLocale, toMode, toPalette, toScope, toTheme } from './prefs.js';
|
|
10
10
|
import { renderBackend } from './renderer-port.js';
|
|
11
11
|
import { ReplayClock } from './replay.js';
|
|
12
12
|
import { ReviewStore, reviewKey } from './review-store.js';
|
|
@@ -23,6 +23,8 @@ export const FLY_MAX_ZOOM = 1.8;
|
|
|
23
23
|
export const FIT_MAX_ZOOM = 2.5;
|
|
24
24
|
/** Minimap marks are re-rasterized at most this often while replaying (ms). */
|
|
25
25
|
const MINIMAP_INTERVAL = 200;
|
|
26
|
+
/** Everything counts as in view (the minimap hides) from this share of the content bounds on. */
|
|
27
|
+
export const OVERVIEW_COVERAGE = 0.98;
|
|
26
28
|
class Superseded extends Error {
|
|
27
29
|
}
|
|
28
30
|
/** Share of `bounds` inside `view`, in [0, 1]. */
|
|
@@ -52,6 +54,7 @@ export class ViewerController {
|
|
|
52
54
|
resolver = null;
|
|
53
55
|
list = EMPTY_LIST;
|
|
54
56
|
mode = 'overlay';
|
|
57
|
+
scope = 'all';
|
|
55
58
|
themePreference = 'auto';
|
|
56
59
|
palette = 'default';
|
|
57
60
|
locale = 'en';
|
|
@@ -81,6 +84,8 @@ export class ViewerController {
|
|
|
81
84
|
/** Canvas clicks select (and double clicks fly); off for embeds without controls. */
|
|
82
85
|
selectOnClick = true;
|
|
83
86
|
#labelsInside = false;
|
|
87
|
+
/** The whole content is in view (the minimap has nothing to add). */
|
|
88
|
+
overview = true;
|
|
84
89
|
#review = null;
|
|
85
90
|
#searchIndex = null;
|
|
86
91
|
#renderer = null;
|
|
@@ -98,6 +103,7 @@ export class ViewerController {
|
|
|
98
103
|
#followed = '';
|
|
99
104
|
#loadToken = 0;
|
|
100
105
|
#pendingHunk = null;
|
|
106
|
+
#boundsCache = null;
|
|
101
107
|
#chrome = null;
|
|
102
108
|
/** Camera kept across detach / attach (element moved in the DOM). */
|
|
103
109
|
#savedCamera = null;
|
|
@@ -270,10 +276,16 @@ export class ViewerController {
|
|
|
270
276
|
this.locale = view.locale;
|
|
271
277
|
}
|
|
272
278
|
const previousMode = this.mode;
|
|
273
|
-
|
|
279
|
+
const previousScope = this.scope;
|
|
280
|
+
if (!this.explicit.has('mode'))
|
|
281
|
+
this.mode = view?.mode ?? 'overlay';
|
|
282
|
+
if (layout.scope === 'changes') {
|
|
283
|
+
// Only the changes are laid out: there is no whole graph to show.
|
|
284
|
+
this.scope = 'changes';
|
|
285
|
+
}
|
|
286
|
+
else if (!this.explicit.has('scope')) {
|
|
274
287
|
const threshold = loaded.layoutOptions?.largeGraphThreshold ?? 3000;
|
|
275
|
-
|
|
276
|
-
this.mode = view?.mode ?? (large ? 'changes' : 'overlay');
|
|
288
|
+
this.scope = view?.scope ?? (diff.nodes.length > threshold ? 'changes' : 'all');
|
|
277
289
|
}
|
|
278
290
|
this.#review = new ReviewStore(this.#deps.storage, reviewKey(diff.delta.base.hash, diff.delta.target.hash), diff.hunks.map((h) => h.id));
|
|
279
291
|
this.#rebuildScene();
|
|
@@ -283,15 +295,16 @@ export class ViewerController {
|
|
|
283
295
|
// Learn early whether out-of-layout groups can be expanded (standalone reports).
|
|
284
296
|
loadCompute().then(() => this.#notify('focus'), () => this.#notify('focus'));
|
|
285
297
|
}
|
|
286
|
-
this.#notify('status', 'data', 'mode', 'selection', 'filters', 'review', 'replay', 'theme', 'locale', 'focus', 'swipe');
|
|
298
|
+
this.#notify('status', 'data', 'mode', 'selection', 'filters', 'review', 'replay', 'theme', 'locale', 'focus', 'swipe', 'overview');
|
|
287
299
|
// A new diff starts fitted, after the UI has shown its chrome (the notification above is
|
|
288
300
|
// delivered first) and once the canvas has a size (see `layoutChanged`).
|
|
289
301
|
this.#needsFit = true;
|
|
290
302
|
this.#savedCamera = null;
|
|
291
303
|
queueMicrotask(() => this.layoutChanged());
|
|
292
304
|
this.#host.ready(diff.stats);
|
|
293
|
-
if (this.mode !== previousMode)
|
|
294
|
-
this.#host.mode(this.mode);
|
|
305
|
+
if (this.mode !== previousMode || this.scope !== previousScope) {
|
|
306
|
+
this.#host.mode(this.mode, this.scope);
|
|
307
|
+
}
|
|
295
308
|
const pending = this.#pendingHunk;
|
|
296
309
|
this.#pendingHunk = null;
|
|
297
310
|
if (pending !== null)
|
|
@@ -334,7 +347,7 @@ export class ViewerController {
|
|
|
334
347
|
}
|
|
335
348
|
#updateFocus() {
|
|
336
349
|
const scene = this.scene;
|
|
337
|
-
if (!scene || this.
|
|
350
|
+
if (!scene || this.scope !== 'changes') {
|
|
338
351
|
this.focus = null;
|
|
339
352
|
return;
|
|
340
353
|
}
|
|
@@ -364,6 +377,7 @@ export class ViewerController {
|
|
|
364
377
|
this.#unbind.push(renderer.on('hover', (pick) => this.#onHover(pick)), renderer.on('click', ({ pick }) => this.#onCanvasClick(pick)), renderer.on('dblclick', ({ pick }) => this.#onCanvasDblClick(pick)), renderer.on('camera', ({ source }) => {
|
|
365
378
|
if (source === 'user')
|
|
366
379
|
this.noteUserCamera();
|
|
380
|
+
this.#updateOverview();
|
|
367
381
|
}));
|
|
368
382
|
this.#minimapDirty = true;
|
|
369
383
|
this.invalidate();
|
|
@@ -410,6 +424,7 @@ export class ViewerController {
|
|
|
410
424
|
* `autoFit`) fit again unless the user moved the camera since the last fit.
|
|
411
425
|
*/
|
|
412
426
|
layoutChanged() {
|
|
427
|
+
this.#updateOverview();
|
|
413
428
|
if (this.status !== 'ready')
|
|
414
429
|
return;
|
|
415
430
|
if (!this.#needsFit) {
|
|
@@ -570,27 +585,70 @@ export class ViewerController {
|
|
|
570
585
|
duration: this.theme.motion.base,
|
|
571
586
|
};
|
|
572
587
|
}
|
|
573
|
-
// ───
|
|
588
|
+
// ─── View: mode × scope ───────────────────────────────────────────────────
|
|
589
|
+
/** Whether the scope is fixed to `changes` (the layout covers only the changes). */
|
|
590
|
+
get scopeLocked() {
|
|
591
|
+
return this.layout?.scope === 'changes';
|
|
592
|
+
}
|
|
593
|
+
/** Switch how the versions are compared. The legacy `changes` means overlay + `changes` scope. */
|
|
574
594
|
setMode(value) {
|
|
595
|
+
if (value === LEGACY_CHANGES_MODE) {
|
|
596
|
+
this.setView('overlay', 'changes');
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
575
599
|
const mode = toMode(value);
|
|
576
|
-
if (mode
|
|
600
|
+
if (mode !== null)
|
|
601
|
+
this.setView(mode, this.scope);
|
|
602
|
+
}
|
|
603
|
+
/** Show the whole graph or only the changes, in any mode. */
|
|
604
|
+
setScope(value) {
|
|
605
|
+
const scope = toScope(value);
|
|
606
|
+
if (scope !== null)
|
|
607
|
+
this.setView(this.mode, scope);
|
|
608
|
+
}
|
|
609
|
+
/** Toggle the `changes` scope (key `4`); says why when the scope is locked. */
|
|
610
|
+
toggleScope() {
|
|
611
|
+
if (this.scopeLocked) {
|
|
612
|
+
this.#host.announce(translate(this.locale, 'scope.locked'));
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
this.setScope(this.scope === 'changes' ? 'all' : 'changes');
|
|
616
|
+
}
|
|
617
|
+
/** Set mode and scope together (one notification, one `kgdiff-mode` event). */
|
|
618
|
+
setView(mode, scope) {
|
|
619
|
+
const nextScope = this.scopeLocked ? 'changes' : scope;
|
|
620
|
+
const modeChanged = mode !== this.mode;
|
|
621
|
+
const scopeChanged = nextScope !== this.scope;
|
|
622
|
+
if (!modeChanged && !scopeChanged)
|
|
577
623
|
return;
|
|
578
624
|
this.mode = mode;
|
|
625
|
+
this.scope = nextScope;
|
|
579
626
|
if (mode !== 'replay') {
|
|
580
627
|
this.clock?.pause();
|
|
581
628
|
this.#restartAt = Number.NaN;
|
|
582
629
|
this.#resume = false;
|
|
583
630
|
}
|
|
584
631
|
this.#followed = '';
|
|
585
|
-
|
|
632
|
+
if (scopeChanged) {
|
|
633
|
+
this.#updateFocus();
|
|
634
|
+
this.#updateOverview();
|
|
635
|
+
}
|
|
586
636
|
this.#minimapDirty = true;
|
|
587
637
|
this.#notify('mode', 'focus', 'replay');
|
|
588
638
|
this.invalidate();
|
|
589
639
|
if (this.status === 'ready') {
|
|
590
|
-
this.#host.mode(mode);
|
|
591
|
-
|
|
640
|
+
this.#host.mode(mode, nextScope);
|
|
641
|
+
const parts = [];
|
|
642
|
+
if (modeChanged) {
|
|
643
|
+
parts.push(translate(this.locale, 'announce.mode', { mode: translate(this.locale, `mode.${mode}`) }));
|
|
644
|
+
}
|
|
645
|
+
if (scopeChanged) {
|
|
646
|
+
parts.push(translate(this.locale, nextScope === 'changes' ? 'scope.on' : 'scope.off'));
|
|
647
|
+
}
|
|
648
|
+
this.#host.announce(parts.join(' · '));
|
|
592
649
|
}
|
|
593
|
-
|
|
650
|
+
// Switching to replay starts a looping replay; toggling the scope leaves playback alone.
|
|
651
|
+
if (modeChanged && mode === 'replay')
|
|
594
652
|
this.#autoplay();
|
|
595
653
|
}
|
|
596
654
|
// ─── Selection ────────────────────────────────────────────────────────────
|
|
@@ -905,6 +963,29 @@ export class ViewerController {
|
|
|
905
963
|
}
|
|
906
964
|
return { bounds: fit.bounds, insets };
|
|
907
965
|
}
|
|
966
|
+
/** Bounds of what is drawn: the scene, or the visible part of the `changes` scope's focus. */
|
|
967
|
+
#contentBounds() {
|
|
968
|
+
const scene = this.scene;
|
|
969
|
+
if (!scene)
|
|
970
|
+
return null;
|
|
971
|
+
const focus = this.focus;
|
|
972
|
+
const cached = this.#boundsCache;
|
|
973
|
+
if (cached && cached.scene === scene && cached.focus === focus)
|
|
974
|
+
return cached.rect;
|
|
975
|
+
const rect = focus ? focusBounds(scene, focus) : frameBounds(scene);
|
|
976
|
+
this.#boundsCache = { scene, focus, rect };
|
|
977
|
+
return rect;
|
|
978
|
+
}
|
|
979
|
+
/** Whether everything drawn is in view; the minimap hides while it is. */
|
|
980
|
+
#updateOverview() {
|
|
981
|
+
const bounds = this.#contentBounds();
|
|
982
|
+
const view = this.#renderer?.camera.viewport();
|
|
983
|
+
const next = !bounds || !view || coverage(bounds, view) >= OVERVIEW_COVERAGE;
|
|
984
|
+
if (next === this.overview)
|
|
985
|
+
return;
|
|
986
|
+
this.overview = next;
|
|
987
|
+
this.#notify('overview');
|
|
988
|
+
}
|
|
908
989
|
zoom(direction) {
|
|
909
990
|
this.noteUserCamera();
|
|
910
991
|
this.#renderer?.camera.zoomBy(direction > 0 ? 1.4 : 1 / 1.4);
|
|
@@ -1012,6 +1093,7 @@ export class ViewerController {
|
|
|
1012
1093
|
return;
|
|
1013
1094
|
this.hops = h;
|
|
1014
1095
|
this.#updateFocus();
|
|
1096
|
+
this.#updateOverview();
|
|
1015
1097
|
this.#minimapDirty = true;
|
|
1016
1098
|
this.#notify('focus');
|
|
1017
1099
|
this.invalidate();
|
|
@@ -1141,6 +1223,7 @@ export class ViewerController {
|
|
|
1141
1223
|
}
|
|
1142
1224
|
#focusChanged() {
|
|
1143
1225
|
this.#updateFocus();
|
|
1226
|
+
this.#updateOverview();
|
|
1144
1227
|
this.#minimapDirty = true;
|
|
1145
1228
|
this.#notify('focus');
|
|
1146
1229
|
this.invalidate();
|
|
@@ -1394,6 +1477,28 @@ function isString(x) {
|
|
|
1394
1477
|
function sameInsets(a, b) {
|
|
1395
1478
|
return a.top === b.top && a.right === b.right && a.bottom === b.bottom && a.left === b.left;
|
|
1396
1479
|
}
|
|
1480
|
+
/** Bounds of the visible part of `focus`, with room for labels and the collapsed pills. */
|
|
1481
|
+
function focusBounds(scene, focus) {
|
|
1482
|
+
let x0 = Number.POSITIVE_INFINITY;
|
|
1483
|
+
let y0 = Number.POSITIVE_INFINITY;
|
|
1484
|
+
let x1 = Number.NEGATIVE_INFINITY;
|
|
1485
|
+
let y1 = Number.NEGATIVE_INFINITY;
|
|
1486
|
+
const add = (x, y, rx, top, bottom) => {
|
|
1487
|
+
x0 = Math.min(x0, x - rx);
|
|
1488
|
+
x1 = Math.max(x1, x + rx);
|
|
1489
|
+
y0 = Math.min(y0, y - top);
|
|
1490
|
+
y1 = Math.max(y1, y + bottom);
|
|
1491
|
+
};
|
|
1492
|
+
// Labels sit centered below their node: leave room for a typical one.
|
|
1493
|
+
for (const n of scene.nodes)
|
|
1494
|
+
if (focus.nodes[n.index] === 1)
|
|
1495
|
+
add(n.x, n.y, n.r + 48, n.r, n.r + 24);
|
|
1496
|
+
for (const g of focus.collapsed)
|
|
1497
|
+
add(g.x, g.y, 48, 12, 12);
|
|
1498
|
+
if (!(x0 <= x1))
|
|
1499
|
+
return frameBounds(scene);
|
|
1500
|
+
return padRect({ x: x0, y: y0, width: x1 - x0, height: y1 - y0 }, 160, 1.12);
|
|
1501
|
+
}
|
|
1397
1502
|
/** Pad a small rect so a node is shown with its neighbourhood. */
|
|
1398
1503
|
function padRect(r, min = 200, ratio = 1.4) {
|
|
1399
1504
|
const w = Math.max(min, r.width * ratio);
|