@navaramap/font 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/dist/index.d.ts +59 -5
- package/dist/index.js +229 -186
- package/dist/index.umd.cjs +1 -1
- package/package.json +6 -6
- package/src/FontManager.test.ts +5 -15
- package/src/FontManager.ts +20 -13
- package/src/FontWorkerClient.test.ts +114 -0
- package/src/FontWorkerClient.ts +62 -23
- package/src/cssFontFamily.ts +12 -6
- package/src/fontWorker.ts +3 -0
- package/src/index.ts +2 -0
- package/src/types.ts +34 -0
package/src/FontManager.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { ConcurrencyManager } from "@navaramap/worker";
|
|
2
1
|
import {
|
|
3
2
|
DataTexture,
|
|
4
3
|
LinearFilter,
|
|
@@ -92,9 +91,10 @@ export class FontManager {
|
|
|
92
91
|
private _workerUrl: string | URL;
|
|
93
92
|
private _client: FontWorkerClient | undefined;
|
|
94
93
|
private _clientPromise: Promise<FontWorkerClient> | undefined;
|
|
95
|
-
private _concurrencyManager: ConcurrencyManager | undefined;
|
|
96
94
|
/** Worker cache budget, delivered when the (lazy) client is created. */
|
|
97
95
|
private _fontBudgetBytes: number | undefined;
|
|
96
|
+
/** Delivered when the (lazy) client is created; see `setWorkerCoreReservation`. */
|
|
97
|
+
private _reserveWorkerCore: (() => () => void) | undefined;
|
|
98
98
|
/** Tracks in-flight fetch promises to avoid duplicate requests. */
|
|
99
99
|
private _pending = new Map<string, Promise<void>>();
|
|
100
100
|
/** Tracks fonts that have been successfully loaded. */
|
|
@@ -167,10 +167,6 @@ export class FontManager {
|
|
|
167
167
|
this._workerUrl = workerUrl;
|
|
168
168
|
}
|
|
169
169
|
|
|
170
|
-
setConcurrencyManager(concurrencyManager: ConcurrencyManager) {
|
|
171
|
-
this._concurrencyManager = concurrencyManager;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
170
|
/**
|
|
175
171
|
* Register a font family with multiple faces.
|
|
176
172
|
* No font files are loaded until text is prepared.
|
|
@@ -282,15 +278,10 @@ export class FontManager {
|
|
|
282
278
|
private _ensureClient(): Promise<FontWorkerClient> {
|
|
283
279
|
if (this._client) return Promise.resolve(this._client);
|
|
284
280
|
if (this._clientPromise) return this._clientPromise;
|
|
285
|
-
if (!this._concurrencyManager) {
|
|
286
|
-
return Promise.reject(
|
|
287
|
-
new Error("FontManager: concurrencyManager not set"),
|
|
288
|
-
);
|
|
289
|
-
}
|
|
290
|
-
const cm = this._concurrencyManager;
|
|
291
281
|
this._clientPromise = (async () => {
|
|
292
|
-
const client = new FontWorkerClient(this._workerUrl,
|
|
282
|
+
const client = new FontWorkerClient(this._workerUrl, {
|
|
293
283
|
fontBudgetBytes: this._fontBudgetBytes,
|
|
284
|
+
reserveWorkerCore: this._reserveWorkerCore,
|
|
294
285
|
});
|
|
295
286
|
await client.ready();
|
|
296
287
|
this._client = client;
|
|
@@ -311,6 +302,16 @@ export class FontManager {
|
|
|
311
302
|
this._fontBudgetBytes = bytes;
|
|
312
303
|
}
|
|
313
304
|
|
|
305
|
+
/**
|
|
306
|
+
* Lets the font worker hold a core of the view's worker pool while it
|
|
307
|
+
* loads fonts or shapes text (see `reserveWorkerCore` in
|
|
308
|
+
* `@navaramap/worker`). Must be called before the first font loads, like
|
|
309
|
+
* {@link setMemoryBudget}.
|
|
310
|
+
*/
|
|
311
|
+
setWorkerCoreReservation(reserve: () => () => void): void {
|
|
312
|
+
this._reserveWorkerCore = reserve;
|
|
313
|
+
}
|
|
314
|
+
|
|
314
315
|
/**
|
|
315
316
|
* Snapshot of the font worker's memory usage, or `undefined` while no
|
|
316
317
|
* worker has been spawned (no fonts in use = zero cost).
|
|
@@ -604,6 +605,9 @@ export class FontManager {
|
|
|
604
605
|
let ascender = 0;
|
|
605
606
|
let descender = 0;
|
|
606
607
|
let lineGap = 0;
|
|
608
|
+
// Each face run reports on its own substring, so the label allows spacing
|
|
609
|
+
// only when every run does.
|
|
610
|
+
let allowsLetterSpacing = true;
|
|
607
611
|
|
|
608
612
|
for (const seg of segments) {
|
|
609
613
|
const result = this._shapeCache
|
|
@@ -624,6 +628,7 @@ export class FontManager {
|
|
|
624
628
|
lineGap = result.lineGap;
|
|
625
629
|
}
|
|
626
630
|
const scale = targetUnitsPerEm / result.unitsPerEm;
|
|
631
|
+
allowsLetterSpacing &&= result.allowsLetterSpacing;
|
|
627
632
|
|
|
628
633
|
for (const g of result.glyphs) {
|
|
629
634
|
allGlyphs.push(
|
|
@@ -659,6 +664,7 @@ export class FontManager {
|
|
|
659
664
|
ascender: 800,
|
|
660
665
|
descender: -200,
|
|
661
666
|
lineGap: 0,
|
|
667
|
+
allowsLetterSpacing,
|
|
662
668
|
};
|
|
663
669
|
}
|
|
664
670
|
|
|
@@ -669,6 +675,7 @@ export class FontManager {
|
|
|
669
675
|
ascender,
|
|
670
676
|
descender,
|
|
671
677
|
lineGap,
|
|
678
|
+
allowsLetterSpacing,
|
|
672
679
|
};
|
|
673
680
|
}
|
|
674
681
|
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { afterEach, beforeEach, expect, it, vi } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { FontWorkerClient } from "./FontWorkerClient";
|
|
4
|
+
|
|
5
|
+
const mocks = vi.hoisted(() => ({
|
|
6
|
+
reserveWorkerCore: vi.fn(),
|
|
7
|
+
release: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
|
|
10
|
+
type Message = { id: number; type: string };
|
|
11
|
+
|
|
12
|
+
/** A worker that answers only when a test tells it to. */
|
|
13
|
+
class MockWorker {
|
|
14
|
+
static last: MockWorker;
|
|
15
|
+
onmessage: ((e: MessageEvent) => void) | null = null;
|
|
16
|
+
onerror: ((e: unknown) => void) | null = null;
|
|
17
|
+
received: Message[] = [];
|
|
18
|
+
constructor() {
|
|
19
|
+
MockWorker.last = this;
|
|
20
|
+
}
|
|
21
|
+
postMessage(message: Message) {
|
|
22
|
+
this.received.push(message);
|
|
23
|
+
}
|
|
24
|
+
terminate() {}
|
|
25
|
+
/** Answer the oldest unanswered request of `type`. */
|
|
26
|
+
reply(type: string) {
|
|
27
|
+
const index = this.received.findIndex((m) => m.type === type);
|
|
28
|
+
const [message] = this.received.splice(index, 1);
|
|
29
|
+
this.onmessage?.({
|
|
30
|
+
data: { id: message.id, type: "ok", payload: { ok: true, results: [] } },
|
|
31
|
+
} as MessageEvent);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.useFakeTimers();
|
|
37
|
+
vi.stubGlobal("Worker", MockWorker);
|
|
38
|
+
mocks.reserveWorkerCore.mockReset().mockReturnValue(mocks.release);
|
|
39
|
+
mocks.release.mockReset();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
afterEach(() => {
|
|
43
|
+
vi.useRealTimers();
|
|
44
|
+
vi.unstubAllGlobals();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("holds one pool core while heavy requests run and releases it after a short idle", async () => {
|
|
48
|
+
const client = new FontWorkerClient("font-worker.js", {
|
|
49
|
+
reserveWorkerCore: mocks.reserveWorkerCore,
|
|
50
|
+
});
|
|
51
|
+
const worker = MockWorker.last;
|
|
52
|
+
expect(mocks.reserveWorkerCore).toHaveBeenCalledOnce();
|
|
53
|
+
|
|
54
|
+
const shaping = client.prepareTextBatch("font.ttf", ["a"]);
|
|
55
|
+
worker.reply("init");
|
|
56
|
+
await vi.advanceTimersByTimeAsync(200);
|
|
57
|
+
// Shaping is still in flight.
|
|
58
|
+
expect(mocks.release).not.toHaveBeenCalled();
|
|
59
|
+
|
|
60
|
+
worker.reply("prepareTextBatch");
|
|
61
|
+
await shaping;
|
|
62
|
+
await vi.advanceTimersByTimeAsync(50);
|
|
63
|
+
// A request within the delay keeps the same reservation.
|
|
64
|
+
const loading = client.loadFont(
|
|
65
|
+
"b.ttf",
|
|
66
|
+
new ArrayBuffer(1),
|
|
67
|
+
undefined,
|
|
68
|
+
false,
|
|
69
|
+
);
|
|
70
|
+
worker.reply("loadFont");
|
|
71
|
+
await loading;
|
|
72
|
+
await vi.advanceTimersByTimeAsync(99);
|
|
73
|
+
expect(mocks.release).not.toHaveBeenCalled();
|
|
74
|
+
expect(mocks.reserveWorkerCore).toHaveBeenCalledOnce();
|
|
75
|
+
|
|
76
|
+
await vi.advanceTimersByTimeAsync(1);
|
|
77
|
+
expect(mocks.release).toHaveBeenCalledOnce();
|
|
78
|
+
|
|
79
|
+
client.dispose();
|
|
80
|
+
expect(mocks.release).toHaveBeenCalledOnce();
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("does not hold a core for glyph reference updates", async () => {
|
|
84
|
+
const client = new FontWorkerClient("font-worker.js", {
|
|
85
|
+
reserveWorkerCore: mocks.reserveWorkerCore,
|
|
86
|
+
});
|
|
87
|
+
const worker = MockWorker.last;
|
|
88
|
+
worker.reply("init");
|
|
89
|
+
await vi.advanceTimersByTimeAsync(100);
|
|
90
|
+
expect(mocks.release).toHaveBeenCalledOnce();
|
|
91
|
+
|
|
92
|
+
client.retainGlyphs("atlas", new BigUint64Array([1n]));
|
|
93
|
+
expect(mocks.reserveWorkerCore).toHaveBeenCalledOnce();
|
|
94
|
+
worker.reply("retainGlyphs");
|
|
95
|
+
client.dispose();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("holds no core without a reservation function", () => {
|
|
99
|
+
const client = new FontWorkerClient("font-worker.js");
|
|
100
|
+
MockWorker.last.reply("init");
|
|
101
|
+
client.dispose();
|
|
102
|
+
expect(mocks.reserveWorkerCore).not.toHaveBeenCalled();
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("releases the core at once when disposed", async () => {
|
|
106
|
+
const client = new FontWorkerClient("font-worker.js", {
|
|
107
|
+
reserveWorkerCore: mocks.reserveWorkerCore,
|
|
108
|
+
});
|
|
109
|
+
// The unanswered init is rejected by the dispose.
|
|
110
|
+
const ready = client.ready().catch(() => undefined);
|
|
111
|
+
client.dispose();
|
|
112
|
+
expect(mocks.release).toHaveBeenCalledOnce();
|
|
113
|
+
await ready;
|
|
114
|
+
});
|
package/src/FontWorkerClient.ts
CHANGED
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import type { ConcurrencyManager } from "@navaramap/worker";
|
|
2
|
-
|
|
3
1
|
import type {
|
|
4
2
|
ShapeTextResult,
|
|
5
3
|
FontAtlasData,
|
|
@@ -11,29 +9,38 @@ export type FontWorkerClientOptions = {
|
|
|
11
9
|
/** Memory budget for the worker's font caches (font data + atlas pixels).
|
|
12
10
|
* Caps further atlas growth; the WASM heap itself never shrinks. */
|
|
13
11
|
fontBudgetBytes?: number;
|
|
12
|
+
/** Holds a worker-pool core while the font worker is busy and returns
|
|
13
|
+
* its release (`reserveWorkerCore` of the view's worker pool). Passed in
|
|
14
|
+
* rather than imported: this package bundles its own copy of
|
|
15
|
+
* `@navaramap/worker`, which would hold a core on a pool that does not
|
|
16
|
+
* exist. */
|
|
17
|
+
reserveWorkerCore?: () => () => void;
|
|
14
18
|
};
|
|
15
19
|
|
|
20
|
+
/** How long the worker's core stays held after its last heavy request, so a
|
|
21
|
+
* burst of requests does not flip the reservation per request. */
|
|
22
|
+
const CORE_RELEASE_DELAY_MS = 100;
|
|
23
|
+
|
|
16
24
|
/**
|
|
17
25
|
* Main-thread client that communicates with the dedicated font Web Worker.
|
|
18
26
|
* Uses a request/response protocol with incrementing message IDs.
|
|
19
27
|
*/
|
|
20
28
|
export class FontWorkerClient {
|
|
21
29
|
private _worker: Worker;
|
|
22
|
-
private _concurrencyManager: ConcurrencyManager;
|
|
23
30
|
private _nextId = 0;
|
|
24
31
|
private _pending = new Map<
|
|
25
32
|
number,
|
|
26
33
|
{ resolve: (value: unknown) => void; reject: (reason: unknown) => void }
|
|
27
34
|
>();
|
|
28
35
|
private _ready: Promise<void>;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
36
|
+
/** Heavy requests (init, font loads, text shaping) still in flight. */
|
|
37
|
+
private _heavyRequests = 0;
|
|
38
|
+
private _releaseCore: (() => void) | undefined;
|
|
39
|
+
private _releaseCoreTimer: ReturnType<typeof setTimeout> | undefined;
|
|
40
|
+
private _reserveWorkerCore: (() => () => void) | undefined;
|
|
41
|
+
|
|
42
|
+
constructor(workerUrl: string | URL, options?: FontWorkerClientOptions) {
|
|
43
|
+
this._reserveWorkerCore = options?.reserveWorkerCore;
|
|
37
44
|
this._worker = new Worker(workerUrl, {
|
|
38
45
|
type: import.meta.env.PROD ? undefined : "module",
|
|
39
46
|
});
|
|
@@ -59,10 +66,11 @@ export class FontWorkerClient {
|
|
|
59
66
|
|
|
60
67
|
// Trigger WASM init and resolve once the worker is ready; the init
|
|
61
68
|
// payload also delivers the memory budget.
|
|
62
|
-
this._ready = this.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
69
|
+
this._ready = this._holdCore(
|
|
70
|
+
this._send("init", {
|
|
71
|
+
fontBudgetBytes: options?.fontBudgetBytes,
|
|
72
|
+
}),
|
|
73
|
+
).then(() => undefined);
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
/** Wait for the WASM module to be initialized in the worker. */
|
|
@@ -87,9 +95,9 @@ export class FontWorkerClient {
|
|
|
87
95
|
atlasKey: string | undefined,
|
|
88
96
|
highQuality: boolean,
|
|
89
97
|
): Promise<{ ok: boolean; cmapRanges: Uint32Array | null }> {
|
|
90
|
-
return this.
|
|
91
|
-
data,
|
|
92
|
-
|
|
98
|
+
return this._holdCore(
|
|
99
|
+
this._send("loadFont", { url, data, atlasKey, highQuality }, [data]),
|
|
100
|
+
) as Promise<{
|
|
93
101
|
ok: boolean;
|
|
94
102
|
cmapRanges: Uint32Array | null;
|
|
95
103
|
}>;
|
|
@@ -122,10 +130,12 @@ export class FontWorkerClient {
|
|
|
122
130
|
height: number;
|
|
123
131
|
channels: number;
|
|
124
132
|
};
|
|
125
|
-
const raw = (await this.
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
133
|
+
const raw = (await this._holdCore(
|
|
134
|
+
this._send("prepareTextBatch", {
|
|
135
|
+
fontUrl,
|
|
136
|
+
texts,
|
|
137
|
+
}),
|
|
138
|
+
)) as {
|
|
129
139
|
results: { text: string; shapeResult: ShapeTextResult | null }[];
|
|
130
140
|
atlas: RawAtlas | null;
|
|
131
141
|
colorAtlas: RawAtlas | null;
|
|
@@ -161,12 +171,41 @@ export class FontWorkerClient {
|
|
|
161
171
|
}
|
|
162
172
|
|
|
163
173
|
dispose(): void {
|
|
174
|
+
if (this._releaseCoreTimer !== undefined) {
|
|
175
|
+
clearTimeout(this._releaseCoreTimer);
|
|
176
|
+
this._releaseCoreTimer = undefined;
|
|
177
|
+
}
|
|
178
|
+
this._releaseCore?.();
|
|
179
|
+
this._releaseCore = undefined;
|
|
164
180
|
this._worker.terminate();
|
|
165
181
|
for (const pending of this._pending.values()) {
|
|
166
182
|
pending.reject(new Error("FontWorkerClient disposed"));
|
|
167
183
|
}
|
|
168
184
|
this._pending.clear();
|
|
169
|
-
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Hold a worker-pool core while `request` keeps the font worker busy, so
|
|
189
|
+
* the pool leaves the worker a core of its own (see `reserveWorkerCore`).
|
|
190
|
+
*/
|
|
191
|
+
private _holdCore(request: Promise<unknown>): Promise<unknown> {
|
|
192
|
+
if (this._releaseCoreTimer !== undefined) {
|
|
193
|
+
clearTimeout(this._releaseCoreTimer);
|
|
194
|
+
this._releaseCoreTimer = undefined;
|
|
195
|
+
}
|
|
196
|
+
this._releaseCore ??= this._reserveWorkerCore?.();
|
|
197
|
+
this._heavyRequests++;
|
|
198
|
+
const settle = () => {
|
|
199
|
+
this._heavyRequests--;
|
|
200
|
+
if (this._heavyRequests > 0 || !this._releaseCore) return;
|
|
201
|
+
this._releaseCoreTimer = setTimeout(() => {
|
|
202
|
+
this._releaseCoreTimer = undefined;
|
|
203
|
+
this._releaseCore?.();
|
|
204
|
+
this._releaseCore = undefined;
|
|
205
|
+
}, CORE_RELEASE_DELAY_MS);
|
|
206
|
+
};
|
|
207
|
+
request.then(settle, settle);
|
|
208
|
+
return request;
|
|
170
209
|
}
|
|
171
210
|
|
|
172
211
|
private _send(
|
package/src/cssFontFamily.ts
CHANGED
|
@@ -41,13 +41,19 @@ export type FetchCssFontFamilyOptions = CssFontFaceFilter & {
|
|
|
41
41
|
// initialize that WASM module and delegate. Parsing correctness is covered by
|
|
42
42
|
// the crate's Rust tests; the TS tests here cover the fetch orchestration.
|
|
43
43
|
//
|
|
44
|
-
// The WASM module is instantiated lazily and at most once
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
|
|
44
|
+
// The WASM module is instantiated lazily and at most once. `__wbg_init` is
|
|
45
|
+
// idempotent only once resolved, and a second instantiation while the first
|
|
46
|
+
// is in flight replaces the module other callers' objects live in, so every
|
|
47
|
+
// caller, including @navaramap/three-api's `initNavaraApi`, shares one
|
|
48
|
+
// promise under a global key.
|
|
49
|
+
const API_READY = Symbol.for("@navaramap/engine-api/init");
|
|
50
|
+
type ApiReadyHolder = { [API_READY]?: Promise<unknown> };
|
|
49
51
|
function ensureApi(): Promise<unknown> {
|
|
50
|
-
|
|
52
|
+
const holder = globalThis as ApiReadyHolder;
|
|
53
|
+
return (holder[API_READY] ??= initApi().catch((e: unknown) => {
|
|
54
|
+
holder[API_READY] = undefined;
|
|
55
|
+
throw e;
|
|
56
|
+
}));
|
|
51
57
|
}
|
|
52
58
|
|
|
53
59
|
/**
|
package/src/fontWorker.ts
CHANGED
|
@@ -32,6 +32,8 @@ function convertGlyphs(glyphs: WasmShapedGlyph[]) {
|
|
|
32
32
|
xOffset: g.x_offset,
|
|
33
33
|
yOffset: g.y_offset,
|
|
34
34
|
charClass: g.char_class,
|
|
35
|
+
continuesCluster: g.continues_cluster,
|
|
36
|
+
direction: g.direction,
|
|
35
37
|
};
|
|
36
38
|
g.free();
|
|
37
39
|
return out;
|
|
@@ -66,6 +68,7 @@ function convertShapeResult(sr: WasmShapeTextResult | undefined) {
|
|
|
66
68
|
ascender: sr.ascender,
|
|
67
69
|
descender: sr.descender,
|
|
68
70
|
lineGap: sr.line_gap,
|
|
71
|
+
allowsLetterSpacing: sr.allows_letter_spacing,
|
|
69
72
|
};
|
|
70
73
|
sr.free();
|
|
71
74
|
return result;
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -23,6 +23,16 @@ export const SDF_PX_SIZE = 64.0;
|
|
|
23
23
|
export const atlasRangePx = (highQuality: boolean): number =>
|
|
24
24
|
highQuality ? 16.0 : 35.0;
|
|
25
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Pixels of distance field a quality's glyph rect keeps around the outline.
|
|
28
|
+
* The text shader grows a glyph's quad past it for wider outlines.
|
|
29
|
+
*
|
|
30
|
+
* SDF: `SDF_BUFFER` (12) in `crates/navara_wasm_font_worker/src/atlas.rs`.
|
|
31
|
+
* MSDF: `MSDF_PADDING_PX` (8) in `crates/navara_wasm_font_worker/src/msdf.rs`.
|
|
32
|
+
*/
|
|
33
|
+
export const atlasPaddingPx = (highQuality: boolean): number =>
|
|
34
|
+
highQuality ? 8.0 : 12.0;
|
|
35
|
+
|
|
26
36
|
/** Glyph metrics from either the SDF or the color atlas. */
|
|
27
37
|
export type GlyphMetrics = {
|
|
28
38
|
glyphId: number;
|
|
@@ -55,6 +65,21 @@ export const GlyphCharClass = {
|
|
|
55
65
|
Ideographic: 3,
|
|
56
66
|
} as const;
|
|
57
67
|
|
|
68
|
+
/**
|
|
69
|
+
* Direction of the `\n`-separated segment a glyph was shaped in. Must match
|
|
70
|
+
* the `DIRECTION_*` constants in
|
|
71
|
+
* `crates/navara_wasm_font_worker/src/shaping.rs`.
|
|
72
|
+
*/
|
|
73
|
+
export const GlyphDirection = {
|
|
74
|
+
/** No character with a script of its own (digits, punctuation, spaces).
|
|
75
|
+
* Font-family fallback shapes such runs apart from the letters around
|
|
76
|
+
* them, so a line's direction is its first non-neutral glyph's. */
|
|
77
|
+
Neutral: 0,
|
|
78
|
+
Ltr: 1,
|
|
79
|
+
/** The segment's glyphs arrive in visual order (reversed logical order). */
|
|
80
|
+
Rtl: 2,
|
|
81
|
+
} as const;
|
|
82
|
+
|
|
58
83
|
/** A single shaped glyph with positioning info. */
|
|
59
84
|
export type ShapedGlyph = {
|
|
60
85
|
glyphId: number;
|
|
@@ -68,6 +93,11 @@ export type ShapedGlyph = {
|
|
|
68
93
|
yOffset: number;
|
|
69
94
|
/** One of `GlyphCharClass` (line-break info). */
|
|
70
95
|
charClass: number;
|
|
96
|
+
/** Same shaping cluster as the previous glyph (combining marks, pre-base
|
|
97
|
+
* vowels). Letter spacing is inserted only before cluster starts. */
|
|
98
|
+
continuesCluster?: boolean;
|
|
99
|
+
/** One of `GlyphDirection`; absent means neutral. */
|
|
100
|
+
direction?: number;
|
|
71
101
|
};
|
|
72
102
|
|
|
73
103
|
/** Result from shaping text: glyph positions + atlas metrics. */
|
|
@@ -82,6 +112,10 @@ export type ShapeTextResult = {
|
|
|
82
112
|
descender: number;
|
|
83
113
|
/** Extra line gap in font units (hhea). */
|
|
84
114
|
lineGap: number;
|
|
115
|
+
/** False when letter spacing must not be applied: tracking pulls apart the
|
|
116
|
+
* joined letterforms of cursive Arabic, so (like MapLibre) any Arabic
|
|
117
|
+
* character disables it for the whole label. */
|
|
118
|
+
allowsLetterSpacing: boolean;
|
|
85
119
|
/** FontManager-internal: the atlas generation these metrics were built
|
|
86
120
|
* under. When the atlas evicts glyphs its generation bumps, marking older
|
|
87
121
|
* cached results stale so they are re-shaped on next use. Unset on results
|