@kgdiff/renderer 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +53 -2
- package/dist/atlas.d.ts +55 -0
- package/dist/atlas.d.ts.map +1 -0
- package/dist/atlas.js +132 -0
- package/dist/atlas.js.map +1 -0
- package/dist/camera.d.ts +116 -0
- package/dist/camera.d.ts.map +1 -0
- package/dist/camera.js +368 -0
- package/dist/camera.js.map +1 -0
- package/dist/color.d.ts +12 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +51 -0
- package/dist/color.js.map +1 -0
- package/dist/ctx.d.ts +57 -0
- package/dist/ctx.d.ts.map +1 -0
- package/dist/ctx.js +28 -0
- package/dist/ctx.js.map +1 -0
- package/dist/draw/background.d.ts +33 -0
- package/dist/draw/background.d.ts.map +1 -0
- package/dist/draw/background.js +105 -0
- package/dist/draw/background.js.map +1 -0
- package/dist/draw/badges.d.ts +26 -0
- package/dist/draw/badges.d.ts.map +1 -0
- package/dist/draw/badges.js +82 -0
- package/dist/draw/badges.js.map +1 -0
- package/dist/draw/collapsed.d.ts +15 -0
- package/dist/draw/collapsed.d.ts.map +1 -0
- package/dist/draw/collapsed.js +67 -0
- package/dist/draw/collapsed.js.map +1 -0
- package/dist/draw/edges.d.ts +14 -0
- package/dist/draw/edges.d.ts.map +1 -0
- package/dist/draw/edges.js +115 -0
- package/dist/draw/edges.js.map +1 -0
- package/dist/draw/glyphs.d.ts +31 -0
- package/dist/draw/glyphs.d.ts.map +1 -0
- package/dist/draw/glyphs.js +114 -0
- package/dist/draw/glyphs.js.map +1 -0
- package/dist/draw/halos.d.ts +19 -0
- package/dist/draw/halos.d.ts.map +1 -0
- package/dist/draw/halos.js +91 -0
- package/dist/draw/halos.js.map +1 -0
- package/dist/draw/labels.d.ts +55 -0
- package/dist/draw/labels.d.ts.map +1 -0
- package/dist/draw/labels.js +300 -0
- package/dist/draw/labels.js.map +1 -0
- package/dist/draw/link.d.ts +25 -0
- package/dist/draw/link.d.ts.map +1 -0
- package/dist/draw/link.js +122 -0
- package/dist/draw/link.js.map +1 -0
- package/dist/draw/nodes.d.ts +13 -0
- package/dist/draw/nodes.d.ts.map +1 -0
- package/dist/draw/nodes.js +165 -0
- package/dist/draw/nodes.js.map +1 -0
- package/dist/draw/shapes.d.ts +7 -0
- package/dist/draw/shapes.d.ts.map +1 -0
- package/dist/draw/shapes.js +26 -0
- package/dist/draw/shapes.js.map +1 -0
- package/dist/draw/view.d.ts +36 -0
- package/dist/draw/view.d.ts.map +1 -0
- package/dist/draw/view.js +16 -0
- package/dist/draw/view.js.map +1 -0
- package/dist/frame-cache.d.ts +103 -0
- package/dist/frame-cache.d.ts.map +1 -0
- package/dist/frame-cache.js +349 -0
- package/dist/frame-cache.js.map +1 -0
- package/dist/index.d.ts +162 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/input.d.ts +81 -0
- package/dist/input.d.ts.map +1 -0
- package/dist/input.js +329 -0
- package/dist/input.js.map +1 -0
- package/dist/labels.d.ts +103 -0
- package/dist/labels.d.ts.map +1 -0
- package/dist/labels.js +203 -0
- package/dist/labels.js.map +1 -0
- package/dist/math.d.ts +33 -0
- package/dist/math.d.ts.map +1 -0
- package/dist/math.js +138 -0
- package/dist/math.js.map +1 -0
- package/dist/minimap.d.ts +56 -0
- package/dist/minimap.d.ts.map +1 -0
- package/dist/minimap.js +501 -0
- package/dist/minimap.js.map +1 -0
- package/dist/paint-stats.d.ts +17 -0
- package/dist/paint-stats.d.ts.map +1 -0
- package/dist/paint-stats.js +23 -0
- package/dist/paint-stats.js.map +1 -0
- package/dist/paint.d.ts +64 -0
- package/dist/paint.d.ts.map +1 -0
- package/dist/paint.js +197 -0
- package/dist/paint.js.map +1 -0
- package/dist/pick.d.ts +26 -0
- package/dist/pick.d.ts.map +1 -0
- package/dist/pick.js +125 -0
- package/dist/pick.js.map +1 -0
- package/dist/renderer.d.ts +28 -0
- package/dist/renderer.d.ts.map +1 -0
- package/dist/renderer.js +470 -0
- package/dist/renderer.js.map +1 -0
- package/dist/smooth-zoom.d.ts +21 -0
- package/dist/smooth-zoom.d.ts.map +1 -0
- package/dist/smooth-zoom.js +52 -0
- package/dist/smooth-zoom.js.map +1 -0
- package/dist/spatial.d.ts +26 -0
- package/dist/spatial.d.ts.map +1 -0
- package/dist/spatial.js +103 -0
- package/dist/spatial.js.map +1 -0
- package/dist/text.d.ts +36 -0
- package/dist/text.d.ts.map +1 -0
- package/dist/text.js +123 -0
- package/dist/text.js.map +1 -0
- package/package.json +47 -3
- package/src/atlas.ts +171 -0
- package/src/camera.ts +482 -0
- package/src/color.ts +58 -0
- package/src/ctx.ts +102 -0
- package/src/draw/background.ts +139 -0
- package/src/draw/badges.ts +93 -0
- package/src/draw/collapsed.ts +86 -0
- package/src/draw/edges.ts +135 -0
- package/src/draw/glyphs.ts +168 -0
- package/src/draw/halos.ts +101 -0
- package/src/draw/labels.ts +399 -0
- package/src/draw/link.ts +157 -0
- package/src/draw/nodes.ts +184 -0
- package/src/draw/shapes.ts +36 -0
- package/src/draw/view.ts +37 -0
- package/src/frame-cache.ts +382 -0
- package/src/index.ts +161 -0
- package/src/input.ts +370 -0
- package/src/labels.ts +269 -0
- package/src/math.ts +185 -0
- package/src/minimap.ts +545 -0
- package/src/paint-stats.ts +38 -0
- package/src/paint.ts +235 -0
- package/src/pick.ts +173 -0
- package/src/renderer.ts +539 -0
- package/src/smooth-zoom.ts +65 -0
- package/src/spatial.ts +94 -0
- package/src/text.ts +130 -0
package/dist/spatial.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Uniform grid over points (node centres) in compressed-row form: O(n) build with two passes
|
|
3
|
+
* and no per-cell arrays, O(k) rectangle queries. Used for picking and glyph endpoint lookup.
|
|
4
|
+
*/
|
|
5
|
+
const MAX_CELLS_PER_AXIS = 512;
|
|
6
|
+
export class PointGrid {
|
|
7
|
+
x0 = 0;
|
|
8
|
+
y0 = 0;
|
|
9
|
+
cell = 1;
|
|
10
|
+
cols = 0;
|
|
11
|
+
rows = 0;
|
|
12
|
+
start = new Int32Array(1);
|
|
13
|
+
items = new Int32Array(0);
|
|
14
|
+
/** Largest radius among the indexed points (query rectangles grow by it). */
|
|
15
|
+
maxR = 0;
|
|
16
|
+
/** Number of indexed points. */
|
|
17
|
+
size = 0;
|
|
18
|
+
/**
|
|
19
|
+
* Index `n` points. `ids[i]` is reported by queries for the point at (xs[i], ys[i]).
|
|
20
|
+
* The cell size targets about one point per cell, bounded by the largest radius.
|
|
21
|
+
*/
|
|
22
|
+
build(xs, ys, rs, ids, n) {
|
|
23
|
+
this.size = n;
|
|
24
|
+
let minX = Number.POSITIVE_INFINITY;
|
|
25
|
+
let minY = Number.POSITIVE_INFINITY;
|
|
26
|
+
let maxX = Number.NEGATIVE_INFINITY;
|
|
27
|
+
let maxY = Number.NEGATIVE_INFINITY;
|
|
28
|
+
let maxR = 0;
|
|
29
|
+
for (let i = 0; i < n; i++) {
|
|
30
|
+
const x = xs[i];
|
|
31
|
+
const y = ys[i];
|
|
32
|
+
if (x < minX)
|
|
33
|
+
minX = x;
|
|
34
|
+
if (x > maxX)
|
|
35
|
+
maxX = x;
|
|
36
|
+
if (y < minY)
|
|
37
|
+
minY = y;
|
|
38
|
+
if (y > maxY)
|
|
39
|
+
maxY = y;
|
|
40
|
+
if (rs[i] > maxR)
|
|
41
|
+
maxR = rs[i];
|
|
42
|
+
}
|
|
43
|
+
this.maxR = maxR;
|
|
44
|
+
if (n === 0) {
|
|
45
|
+
this.cols = this.rows = 0;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
const w = Math.max(maxX - minX, 1e-9);
|
|
49
|
+
const h = Math.max(maxY - minY, 1e-9);
|
|
50
|
+
let cell = Math.max(Math.sqrt((w * h) / n), 2 * maxR, 1e-6);
|
|
51
|
+
cell = Math.max(cell, w / MAX_CELLS_PER_AXIS, h / MAX_CELLS_PER_AXIS);
|
|
52
|
+
this.cell = cell;
|
|
53
|
+
this.x0 = minX;
|
|
54
|
+
this.y0 = minY;
|
|
55
|
+
this.cols = Math.floor(w / cell) + 1;
|
|
56
|
+
this.rows = Math.floor(h / cell) + 1;
|
|
57
|
+
const cells = this.cols * this.rows;
|
|
58
|
+
if (this.start.length < cells + 1)
|
|
59
|
+
this.start = new Int32Array(cells + 1);
|
|
60
|
+
else
|
|
61
|
+
this.start.fill(0, 0, cells + 1);
|
|
62
|
+
if (this.items.length < n)
|
|
63
|
+
this.items = new Int32Array(Math.max(n, this.items.length * 2));
|
|
64
|
+
const start = this.start;
|
|
65
|
+
for (let i = 0; i < n; i++)
|
|
66
|
+
start[this.cellOf(xs[i], ys[i]) + 1]++;
|
|
67
|
+
for (let c = 0; c < cells; c++)
|
|
68
|
+
start[c + 1] += start[c];
|
|
69
|
+
// Fill using `start` as a cursor, then shift it back.
|
|
70
|
+
for (let i = 0; i < n; i++) {
|
|
71
|
+
const c = this.cellOf(xs[i], ys[i]);
|
|
72
|
+
this.items[start[c]++] = ids[i];
|
|
73
|
+
}
|
|
74
|
+
for (let c = cells; c > 0; c--)
|
|
75
|
+
start[c] = start[c - 1];
|
|
76
|
+
start[0] = 0;
|
|
77
|
+
}
|
|
78
|
+
/** Ids of points whose centre may lie in [x0, x1] × [y0, y1]; returns how many were written. */
|
|
79
|
+
query(x0, y0, x1, y1, out) {
|
|
80
|
+
out.length = 0;
|
|
81
|
+
if (this.cols === 0)
|
|
82
|
+
return 0;
|
|
83
|
+
const c0 = Math.max(0, Math.floor((x0 - this.x0) / this.cell));
|
|
84
|
+
const c1 = Math.min(this.cols - 1, Math.floor((x1 - this.x0) / this.cell));
|
|
85
|
+
const r0 = Math.max(0, Math.floor((y0 - this.y0) / this.cell));
|
|
86
|
+
const r1 = Math.min(this.rows - 1, Math.floor((y1 - this.y0) / this.cell));
|
|
87
|
+
for (let r = r0; r <= r1; r++) {
|
|
88
|
+
for (let c = c0; c <= c1; c++) {
|
|
89
|
+
const cell = r * this.cols + c;
|
|
90
|
+
const end = this.start[cell + 1];
|
|
91
|
+
for (let j = this.start[cell]; j < end; j++)
|
|
92
|
+
out.push(this.items[j]);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return out.length;
|
|
96
|
+
}
|
|
97
|
+
cellOf(x, y) {
|
|
98
|
+
const c = Math.min(this.cols - 1, Math.max(0, Math.floor((x - this.x0) / this.cell)));
|
|
99
|
+
const r = Math.min(this.rows - 1, Math.max(0, Math.floor((y - this.y0) / this.cell)));
|
|
100
|
+
return r * this.cols + c;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=spatial.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"spatial.js","sourceRoot":"","sources":["../src/spatial.ts"],"names":[],"mappings":"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"}
|
package/dist/text.d.ts
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Fonts, cached text measurement and ellipsis fitting for screen-space labels. */
|
|
2
|
+
import type { Theme } from '@kgdiff/scene';
|
|
3
|
+
import type { Ctx2D } from './ctx.js';
|
|
4
|
+
/** Labels longer than this (css px) are truncated with an ellipsis. */
|
|
5
|
+
export declare const LABEL_MAX_WIDTH = 240;
|
|
6
|
+
/** Line box height for a font size (px). */
|
|
7
|
+
export declare function lineHeight(size: number): number;
|
|
8
|
+
/** Interned CSS font strings (the theme's stacks are long; never rebuilt per frame). */
|
|
9
|
+
export declare class Fonts {
|
|
10
|
+
private readonly cache;
|
|
11
|
+
private family;
|
|
12
|
+
private mono;
|
|
13
|
+
constructor(theme: Theme);
|
|
14
|
+
setTheme(theme: Theme): void;
|
|
15
|
+
/** `weight size family` for the label (or mono) stack. */
|
|
16
|
+
get(size: number, weight: number, mono?: boolean): string;
|
|
17
|
+
}
|
|
18
|
+
/** Width estimate when no 2D context exists (tests, workers without OffscreenCanvas). */
|
|
19
|
+
export declare function estimateWidth(font: string, text: string): number;
|
|
20
|
+
/** Measures text with a 2D context, caching widths per font + text. */
|
|
21
|
+
export declare class TextMeasurer {
|
|
22
|
+
private ctx;
|
|
23
|
+
private readonly widths;
|
|
24
|
+
private readonly fitted;
|
|
25
|
+
private count;
|
|
26
|
+
private font;
|
|
27
|
+
constructor(ctx: Ctx2D | null);
|
|
28
|
+
setContext(ctx: Ctx2D | null): void;
|
|
29
|
+
/** Forget everything (fonts finished loading, theme fonts changed). */
|
|
30
|
+
clear(): void;
|
|
31
|
+
width(font: string, text: string): number;
|
|
32
|
+
/** `text`, truncated with “…” so it is at most `maxWidth` px wide (code-point safe). */
|
|
33
|
+
fit(font: string, text: string, maxWidth?: number): string;
|
|
34
|
+
private measure;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=text.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.d.ts","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"AAAA,mFAAmF;AACnF,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAEtC,uEAAuE;AACvE,eAAO,MAAM,eAAe,MAAM,CAAC;AAGnC,4CAA4C;AAC5C,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C;AAED,wFAAwF;AACxF,qBAAa,KAAK;IAChB,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;IACnD,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,IAAI,CAAS;IAErB,YAAY,KAAK,EAAE,KAAK,EAGvB;IAED,QAAQ,CAAC,KAAK,EAAE,KAAK,GAAG,IAAI,CAK3B;IAED,0DAA0D;IAC1D,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,IAAI,UAAQ,GAAG,MAAM,CAQtD;CACF;AAID,yFAAyF;AACzF,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAMhE;AAED,uEAAuE;AACvE,qBAAa,YAAY;IAMX,OAAO,CAAC,GAAG;IALvB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IACjE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA0C;IACjE,OAAO,CAAC,KAAK,CAAK;IAClB,OAAO,CAAC,IAAI,CAAM;IAElB,YAAoB,GAAG,EAAE,KAAK,GAAG,IAAI,EAAI;IAEzC,UAAU,CAAC,GAAG,EAAE,KAAK,GAAG,IAAI,GAAG,IAAI,CAGlC;IAED,uEAAuE;IACvE,KAAK,IAAI,IAAI,CAKZ;IAED,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAmBxC;IAED,wFAAwF;IACxF,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAwB,GAAG,MAAM,CAoB1E;IAED,OAAO,CAAC,OAAO;CAShB"}
|
package/dist/text.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/** Labels longer than this (css px) are truncated with an ellipsis. */
|
|
2
|
+
export const LABEL_MAX_WIDTH = 240;
|
|
3
|
+
const MAX_CACHED = 20_000;
|
|
4
|
+
/** Line box height for a font size (px). */
|
|
5
|
+
export function lineHeight(size) {
|
|
6
|
+
return Math.round(size * 1.3);
|
|
7
|
+
}
|
|
8
|
+
/** Interned CSS font strings (the theme's stacks are long; never rebuilt per frame). */
|
|
9
|
+
export class Fonts {
|
|
10
|
+
cache = new Map();
|
|
11
|
+
family;
|
|
12
|
+
mono;
|
|
13
|
+
constructor(theme) {
|
|
14
|
+
this.family = theme.font.family;
|
|
15
|
+
this.mono = theme.font.mono;
|
|
16
|
+
}
|
|
17
|
+
setTheme(theme) {
|
|
18
|
+
if (theme.font.family === this.family && theme.font.mono === this.mono)
|
|
19
|
+
return;
|
|
20
|
+
this.family = theme.font.family;
|
|
21
|
+
this.mono = theme.font.mono;
|
|
22
|
+
this.cache.clear();
|
|
23
|
+
}
|
|
24
|
+
/** `weight size family` for the label (or mono) stack. */
|
|
25
|
+
get(size, weight, mono = false) {
|
|
26
|
+
const key = (mono ? -1 : 1) * (Math.round(weight) * 10_000 + Math.round(size * 100));
|
|
27
|
+
let font = this.cache.get(key);
|
|
28
|
+
if (font === undefined) {
|
|
29
|
+
font = `${Math.round(weight)} ${Math.round(size * 100) / 100}px ${mono ? this.mono : this.family}`;
|
|
30
|
+
this.cache.set(key, font);
|
|
31
|
+
}
|
|
32
|
+
return font;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
const CJK = /[ᄀ-ᇿ⺀-鿿가-豈--]/;
|
|
36
|
+
/** Width estimate when no 2D context exists (tests, workers without OffscreenCanvas). */
|
|
37
|
+
export function estimateWidth(font, text) {
|
|
38
|
+
const m = /(\d+(?:\.\d+)?)px/.exec(font);
|
|
39
|
+
const size = m ? Number(m[1]) : 12;
|
|
40
|
+
let w = 0;
|
|
41
|
+
for (const ch of text)
|
|
42
|
+
w += CJK.test(ch) ? size : size * 0.58;
|
|
43
|
+
return w;
|
|
44
|
+
}
|
|
45
|
+
/** Measures text with a 2D context, caching widths per font + text. */
|
|
46
|
+
export class TextMeasurer {
|
|
47
|
+
ctx;
|
|
48
|
+
widths = new Map();
|
|
49
|
+
fitted = new Map();
|
|
50
|
+
count = 0;
|
|
51
|
+
font = '';
|
|
52
|
+
constructor(ctx) {
|
|
53
|
+
this.ctx = ctx;
|
|
54
|
+
}
|
|
55
|
+
setContext(ctx) {
|
|
56
|
+
this.ctx = ctx;
|
|
57
|
+
this.clear();
|
|
58
|
+
}
|
|
59
|
+
/** Forget everything (fonts finished loading, theme fonts changed). */
|
|
60
|
+
clear() {
|
|
61
|
+
this.widths.clear();
|
|
62
|
+
this.fitted.clear();
|
|
63
|
+
this.count = 0;
|
|
64
|
+
this.font = '';
|
|
65
|
+
}
|
|
66
|
+
width(font, text) {
|
|
67
|
+
let byText = this.widths.get(font);
|
|
68
|
+
if (!byText) {
|
|
69
|
+
byText = new Map();
|
|
70
|
+
this.widths.set(font, byText);
|
|
71
|
+
}
|
|
72
|
+
let w = byText.get(text);
|
|
73
|
+
if (w === undefined) {
|
|
74
|
+
w = this.measure(font, text);
|
|
75
|
+
if (++this.count > MAX_CACHED) {
|
|
76
|
+
this.widths.clear();
|
|
77
|
+
this.fitted.clear();
|
|
78
|
+
this.count = 0;
|
|
79
|
+
byText = new Map();
|
|
80
|
+
this.widths.set(font, byText);
|
|
81
|
+
}
|
|
82
|
+
byText.set(text, w);
|
|
83
|
+
}
|
|
84
|
+
return w;
|
|
85
|
+
}
|
|
86
|
+
/** `text`, truncated with “…” so it is at most `maxWidth` px wide (code-point safe). */
|
|
87
|
+
fit(font, text, maxWidth = LABEL_MAX_WIDTH) {
|
|
88
|
+
if (this.width(font, text) <= maxWidth)
|
|
89
|
+
return text;
|
|
90
|
+
let byText = this.fitted.get(font);
|
|
91
|
+
if (!byText) {
|
|
92
|
+
byText = new Map();
|
|
93
|
+
this.fitted.set(font, byText);
|
|
94
|
+
}
|
|
95
|
+
const hit = byText.get(text);
|
|
96
|
+
if (hit !== undefined)
|
|
97
|
+
return hit;
|
|
98
|
+
const chars = Array.from(text);
|
|
99
|
+
let lo = 0;
|
|
100
|
+
let hi = chars.length;
|
|
101
|
+
while (lo < hi) {
|
|
102
|
+
const mid = (lo + hi + 1) >> 1;
|
|
103
|
+
if (this.measure(font, `${chars.slice(0, mid).join('').trimEnd()}…`) <= maxWidth)
|
|
104
|
+
lo = mid;
|
|
105
|
+
else
|
|
106
|
+
hi = mid - 1;
|
|
107
|
+
}
|
|
108
|
+
const out = `${chars.slice(0, lo).join('').trimEnd()}…`;
|
|
109
|
+
byText.set(text, out);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
measure(font, text) {
|
|
113
|
+
const ctx = this.ctx;
|
|
114
|
+
if (!ctx)
|
|
115
|
+
return estimateWidth(font, text);
|
|
116
|
+
if (this.font !== font) {
|
|
117
|
+
ctx.font = font;
|
|
118
|
+
this.font = font;
|
|
119
|
+
}
|
|
120
|
+
return ctx.measureText(text).width;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
//# sourceMappingURL=text.js.map
|
package/dist/text.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text.js","sourceRoot":"","sources":["../src/text.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kgdiff/renderer",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "High-DPI Canvas 2D renderer for kgdiff scenes: smooth camera, LOD, label placement, hit testing and minimap.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"kgdiff",
|
|
7
|
+
"canvas",
|
|
8
|
+
"renderer"
|
|
9
|
+
],
|
|
10
|
+
"homepage": "https://github.com/iiwish/kgdiff/tree/main/packages/renderer#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/iiwish/kgdiff/issues"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "git+https://github.com/iiwish/kgdiff.git",
|
|
17
|
+
"directory": "packages/renderer"
|
|
18
|
+
},
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"author": "iiwish",
|
|
21
|
+
"type": "module",
|
|
22
|
+
"sideEffects": false,
|
|
23
|
+
"exports": {
|
|
24
|
+
".": {
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"default": "./dist/index.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"src",
|
|
32
|
+
"!src/**/*.test.ts"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=22"
|
|
36
|
+
},
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"registry": "https://registry.npmjs.org/"
|
|
40
|
+
},
|
|
41
|
+
"dependencies": {
|
|
42
|
+
"@kgdiff/core": "0.1.0",
|
|
43
|
+
"@kgdiff/scene": "0.1.0"
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc -p tsconfig.build.json",
|
|
47
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
48
|
+
"test": "vitest run"
|
|
49
|
+
}
|
|
6
50
|
}
|
package/src/atlas.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label atlas: every label (text + background halo + strike) is rasterized once into a shared
|
|
3
|
+
* offscreen surface and blitted with `drawImage` afterwards. Labels are screen-sized, so
|
|
4
|
+
* panning and zooming never re-rasterize text; `strokeText` halos are expensive and this keeps
|
|
5
|
+
* them off the per-frame path. Shelf packing; the atlas is wiped when full (rare, amortized).
|
|
6
|
+
*/
|
|
7
|
+
import type { Ctx2D, Surface, SurfaceFactory } from './ctx.js';
|
|
8
|
+
|
|
9
|
+
/** css px of transparent margin around each entry (the 3 px halo needs 1.5 px + AA). */
|
|
10
|
+
export const ATLAS_PAD = 2;
|
|
11
|
+
export const TEXT_HALO_WIDTH = 3;
|
|
12
|
+
export const TEXT_HALO_ALPHA = 0.9;
|
|
13
|
+
|
|
14
|
+
export interface AtlasEntry {
|
|
15
|
+
/** Source rectangle in the atlas (device px). */
|
|
16
|
+
readonly sx: number;
|
|
17
|
+
readonly sy: number;
|
|
18
|
+
readonly sw: number;
|
|
19
|
+
readonly sh: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Draw one label line (halo, fill, strike) with its box's top-left at (x, y). */
|
|
23
|
+
export function drawTextLine(
|
|
24
|
+
ctx: Ctx2D,
|
|
25
|
+
text: string,
|
|
26
|
+
font: string,
|
|
27
|
+
color: string,
|
|
28
|
+
halo: string,
|
|
29
|
+
strike: boolean,
|
|
30
|
+
x: number,
|
|
31
|
+
y: number,
|
|
32
|
+
width: number,
|
|
33
|
+
height: number,
|
|
34
|
+
alpha: number,
|
|
35
|
+
): void {
|
|
36
|
+
const cy = y + height / 2;
|
|
37
|
+
ctx.font = font;
|
|
38
|
+
ctx.textAlign = 'left';
|
|
39
|
+
ctx.textBaseline = 'middle';
|
|
40
|
+
ctx.globalAlpha = alpha * TEXT_HALO_ALPHA;
|
|
41
|
+
ctx.strokeStyle = halo;
|
|
42
|
+
ctx.lineWidth = TEXT_HALO_WIDTH;
|
|
43
|
+
ctx.lineJoin = 'round';
|
|
44
|
+
ctx.miterLimit = 2;
|
|
45
|
+
ctx.strokeText(text, x, cy);
|
|
46
|
+
ctx.globalAlpha = alpha;
|
|
47
|
+
ctx.fillStyle = color;
|
|
48
|
+
ctx.fillText(text, x, cy);
|
|
49
|
+
if (strike) {
|
|
50
|
+
const t = Math.max(1, height / 14);
|
|
51
|
+
ctx.fillRect(x - 1, cy - t / 2 + 0.5, width + 2, t);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export class LabelAtlas {
|
|
56
|
+
private surface: Surface | null = null;
|
|
57
|
+
private size = 0;
|
|
58
|
+
private dpr = 1;
|
|
59
|
+
private halo = '#000';
|
|
60
|
+
private shelfX = 0;
|
|
61
|
+
private shelfY = 0;
|
|
62
|
+
private shelfH = 0;
|
|
63
|
+
/** strike → font → color → text → entry */
|
|
64
|
+
private readonly maps: [
|
|
65
|
+
Map<string, Map<string, Map<string, AtlasEntry>>>,
|
|
66
|
+
Map<string, Map<string, Map<string, AtlasEntry>>>,
|
|
67
|
+
] = [new Map(), new Map()];
|
|
68
|
+
/** Bumped every time the atlas is wiped (tests, diagnostics). */
|
|
69
|
+
generation = 0;
|
|
70
|
+
/**
|
|
71
|
+
* New entries allowed until the next `beginFrame` (rasterizing is the expensive part:
|
|
72
|
+
* a zoom that reveals thousands of labels spreads them over a few frames instead of one).
|
|
73
|
+
*/
|
|
74
|
+
private budget = Number.POSITIVE_INFINITY;
|
|
75
|
+
/** Lookups refused this frame because the budget ran out. */
|
|
76
|
+
deferred = 0;
|
|
77
|
+
|
|
78
|
+
constructor(private readonly factory: SurfaceFactory) {}
|
|
79
|
+
|
|
80
|
+
get canvas(): Surface['canvas'] | null {
|
|
81
|
+
return this.surface?.canvas ?? null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Re-key the atlas for a pixel ratio and halo color; wipes it when either changed. */
|
|
85
|
+
configure(dpr: number, halo: string): void {
|
|
86
|
+
if (dpr === this.dpr && halo === this.halo && this.surface) return;
|
|
87
|
+
this.dpr = dpr;
|
|
88
|
+
this.halo = halo;
|
|
89
|
+
const size = Math.min(4096, Math.ceil(1024 * Math.max(1, dpr)));
|
|
90
|
+
if (!this.surface || this.size !== size) {
|
|
91
|
+
this.surface = this.factory(size, size);
|
|
92
|
+
this.size = size;
|
|
93
|
+
}
|
|
94
|
+
this.wipe();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Start a frame with room for `budget` new entries. */
|
|
98
|
+
beginFrame(budget: number): void {
|
|
99
|
+
this.budget = budget;
|
|
100
|
+
this.deferred = 0;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Forget every entry (fonts loaded, theme changed). */
|
|
104
|
+
wipe(): void {
|
|
105
|
+
this.maps[0].clear();
|
|
106
|
+
this.maps[1].clear();
|
|
107
|
+
this.shelfX = 0;
|
|
108
|
+
this.shelfY = 0;
|
|
109
|
+
this.shelfH = 0;
|
|
110
|
+
this.generation++;
|
|
111
|
+
this.surface?.ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
112
|
+
this.surface?.ctx.clearRect(0, 0, this.size, this.size);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The entry for a label line, rasterizing it on first use. Returns null when it cannot be
|
|
117
|
+
* cached (too large, no surface) and `undefined` when this frame's budget is spent.
|
|
118
|
+
*/
|
|
119
|
+
get(
|
|
120
|
+
text: string,
|
|
121
|
+
font: string,
|
|
122
|
+
color: string,
|
|
123
|
+
strike: boolean,
|
|
124
|
+
width: number,
|
|
125
|
+
height: number,
|
|
126
|
+
): AtlasEntry | null | undefined {
|
|
127
|
+
const surface = this.surface;
|
|
128
|
+
if (!surface) return null;
|
|
129
|
+
const byFont = this.maps[strike ? 1 : 0];
|
|
130
|
+
let byColor = byFont.get(font);
|
|
131
|
+
if (!byColor) {
|
|
132
|
+
byColor = new Map();
|
|
133
|
+
byFont.set(font, byColor);
|
|
134
|
+
}
|
|
135
|
+
let byText = byColor.get(color);
|
|
136
|
+
if (!byText) {
|
|
137
|
+
byText = new Map();
|
|
138
|
+
byColor.set(color, byText);
|
|
139
|
+
}
|
|
140
|
+
const hit = byText.get(text);
|
|
141
|
+
if (hit) return hit;
|
|
142
|
+
if (this.budget <= 0) {
|
|
143
|
+
this.deferred++;
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const dpr = this.dpr;
|
|
148
|
+
const sw = Math.ceil((width + 2 * ATLAS_PAD) * dpr);
|
|
149
|
+
const sh = Math.ceil((height + 2 * ATLAS_PAD) * dpr);
|
|
150
|
+
if (sw > this.size || sh > this.size) return null;
|
|
151
|
+
if (this.shelfX + sw > this.size) {
|
|
152
|
+
this.shelfY += this.shelfH + 1;
|
|
153
|
+
this.shelfX = 0;
|
|
154
|
+
this.shelfH = 0;
|
|
155
|
+
}
|
|
156
|
+
if (this.shelfY + sh > this.size) {
|
|
157
|
+
this.wipe();
|
|
158
|
+
return this.get(text, font, color, strike, width, height);
|
|
159
|
+
}
|
|
160
|
+
this.budget--;
|
|
161
|
+
const entry: AtlasEntry = { sx: this.shelfX, sy: this.shelfY, sw, sh };
|
|
162
|
+
const ctx = surface.ctx;
|
|
163
|
+
ctx.setTransform(dpr, 0, 0, dpr, this.shelfX, this.shelfY);
|
|
164
|
+
drawTextLine(ctx, text, font, color, this.halo, strike, ATLAS_PAD, ATLAS_PAD, width, height, 1);
|
|
165
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
166
|
+
this.shelfX += sw + 1;
|
|
167
|
+
this.shelfH = Math.max(this.shelfH, sh);
|
|
168
|
+
byText.set(text, entry);
|
|
169
|
+
return entry;
|
|
170
|
+
}
|
|
171
|
+
}
|