@empty-sekai/sekai-custom-profile-sdk 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/LICENSE +235 -0
- package/LICENSE-EXCEPTION +32 -0
- package/NOTICE +11 -0
- package/README.en.md +456 -0
- package/README.md +458 -0
- package/dist/allium_renderer_wasm.js +14 -0
- package/dist/allium_renderer_wasm.wasm +0 -0
- package/dist/authoring.d.ts +14 -0
- package/dist/authoring.js +34 -0
- package/dist/cache/glyphPersistentCache.d.ts +145 -0
- package/dist/cache/glyphPersistentCache.js +346 -0
- package/dist/cache/indexedDbGlyphRecordStore.d.ts +26 -0
- package/dist/cache/indexedDbGlyphRecordStore.js +214 -0
- package/dist/cache/sessionImageResourceCache.d.ts +39 -0
- package/dist/cache/sessionImageResourceCache.js +156 -0
- package/dist/emscripten.d.ts +11 -0
- package/dist/emscripten.js +1 -0
- package/dist/fontProvider.d.ts +36 -0
- package/dist/fontProvider.js +84 -0
- package/dist/fontSdfAtlas.d.ts +92 -0
- package/dist/fontSdfAtlas.js +296 -0
- package/dist/gpu/browserSemanticResources.d.ts +64 -0
- package/dist/gpu/browserSemanticResources.js +232 -0
- package/dist/gpu/generalTextRenderPlacement.d.ts +8 -0
- package/dist/gpu/generalTextRenderPlacement.js +131 -0
- package/dist/gpu/previewTransformTextureLayout.d.ts +1 -0
- package/dist/gpu/previewTransformTextureLayout.js +12 -0
- package/dist/gpu/semanticCommandGeometry.d.ts +21 -0
- package/dist/gpu/semanticCommandGeometry.js +226 -0
- package/dist/gpu/semanticCommandPlanner.d.ts +131 -0
- package/dist/gpu/semanticCommandPlanner.js +203 -0
- package/dist/gpu/semanticTextGlyphBridge.d.ts +3 -0
- package/dist/gpu/semanticTextGlyphBridge.js +22 -0
- package/dist/gpu/semanticWebglSceneRenderer.d.ts +70 -0
- package/dist/gpu/semanticWebglSceneRenderer.js +211 -0
- package/dist/gpu/slotRanges.d.ts +5 -0
- package/dist/gpu/slotRanges.js +14 -0
- package/dist/gpu/webglSdfAtlasTexture.d.ts +17 -0
- package/dist/gpu/webglSdfAtlasTexture.js +72 -0
- package/dist/gpu/webglSdfGlyphPipeline.d.ts +26 -0
- package/dist/gpu/webglSdfGlyphPipeline.js +267 -0
- package/dist/gpu/webglSemanticCommandExecutor.d.ts +79 -0
- package/dist/gpu/webglSemanticCommandExecutor.js +686 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +8 -0
- package/dist/interaction/numericTextRegions.d.ts +46 -0
- package/dist/interaction/numericTextRegions.js +44 -0
- package/dist/localizationProvider.d.ts +33 -0
- package/dist/localizationProvider.js +74 -0
- package/dist/originPrebuiltSdfAtlasPackage.d.ts +39 -0
- package/dist/originPrebuiltSdfAtlasPackage.js +316 -0
- package/dist/prebuiltSdfAtlas.d.ts +36 -0
- package/dist/prebuiltSdfAtlas.js +261 -0
- package/dist/protocol.d.ts +456 -0
- package/dist/protocol.js +1 -0
- package/dist/renderer.d.ts +218 -0
- package/dist/renderer.js +641 -0
- package/dist/resourceProvider.d.ts +24 -0
- package/dist/resourceProvider.js +41 -0
- package/dist/telemetry/rendererTelemetry.d.ts +158 -0
- package/dist/telemetry/rendererTelemetry.js +232 -0
- package/dist/third-party/freetype/FTL.txt +169 -0
- package/dist/types/atlas.d.ts +90 -0
- package/dist/types/atlas.js +1 -0
- package/dist/types/authoring.d.ts +81 -0
- package/dist/types/authoring.js +1 -0
- package/dist/types/core.d.ts +128 -0
- package/dist/types/core.js +1 -0
- package/dist/types/freeType.d.ts +54 -0
- package/dist/types/freeType.js +13 -0
- package/dist/types/glyph.d.ts +33 -0
- package/dist/types/glyph.js +1 -0
- package/dist/types/layout.d.ts +25 -0
- package/dist/types/layout.js +1 -0
- package/dist/types/text.d.ts +28 -0
- package/dist/types/text.js +1 -0
- package/dist/worker/glyphWorkScheduler.d.ts +29 -0
- package/dist/worker/glyphWorkScheduler.js +107 -0
- package/dist/worker-client.d.ts +114 -0
- package/dist/worker-client.js +413 -0
- package/dist/worker.d.ts +1 -0
- package/dist/worker.js +708 -0
- package/package.json +46 -0
- package/src/atlas.rs +652 -0
- package/src/authoring.ts +42 -0
- package/src/authoring_runtime.rs +303 -0
- package/src/cache/glyphPersistentCache.ts +463 -0
- package/src/cache/indexedDbGlyphRecordStore.ts +238 -0
- package/src/cache/sessionImageResourceCache.ts +186 -0
- package/src/edt.rs +97 -0
- package/src/emscripten.ts +17 -0
- package/src/fontProvider.ts +127 -0
- package/src/fontSdfAtlas.ts +431 -0
- package/src/geometry.rs +149 -0
- package/src/glyph_plan.rs +226 -0
- package/src/gpu/browserSemanticResources.ts +294 -0
- package/src/gpu/generalTextRenderPlacement.ts +175 -0
- package/src/gpu/previewTransformTextureLayout.ts +12 -0
- package/src/gpu/semanticCommandGeometry.ts +260 -0
- package/src/gpu/semanticCommandPlanner.ts +280 -0
- package/src/gpu/semanticTextGlyphBridge.ts +35 -0
- package/src/gpu/semanticWebglSceneRenderer.ts +261 -0
- package/src/gpu/slotRanges.ts +15 -0
- package/src/gpu/webglSdfAtlasTexture.ts +69 -0
- package/src/gpu/webglSdfGlyphPipeline.ts +299 -0
- package/src/gpu/webglSemanticCommandExecutor.ts +739 -0
- package/src/index.ts +84 -0
- package/src/interaction/numericTextRegions.ts +80 -0
- package/src/layout.rs +1882 -0
- package/src/lib.rs +1443 -0
- package/src/localizationProvider.ts +113 -0
- package/src/main.rs +55 -0
- package/src/masterdata_runtime.rs +496 -0
- package/src/originPrebuiltSdfAtlasPackage.ts +403 -0
- package/src/prebuiltSdfAtlas.ts +306 -0
- package/src/protocol.ts +150 -0
- package/src/renderer.ts +792 -0
- package/src/resourceProvider.ts +69 -0
- package/src/scene.rs +665 -0
- package/src/telemetry/rendererTelemetry.ts +358 -0
- package/src/types/atlas.ts +80 -0
- package/src/types/authoring.ts +85 -0
- package/src/types/core.ts +110 -0
- package/src/types/freeType.ts +63 -0
- package/src/types/glyph.ts +33 -0
- package/src/types/layout.ts +28 -0
- package/src/types/text.ts +28 -0
- package/src/worker/glyphWorkScheduler.ts +130 -0
- package/src/worker-client.ts +484 -0
- package/src/worker.ts +895 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export { BrowserRenderer, BrowserRendererError, BrowserScene, } from "./renderer.js";
|
|
2
|
+
export { BrowserAuthoringClient } from "./authoring.js";
|
|
3
|
+
export type { BrowserRendererOptions, MasterDataTableLoader, MasterDataTableRequest, ProfileSceneCreateOptions, } from "./renderer.js";
|
|
4
|
+
export type { ProvidedResource, ResourceContext, ResourceDescriptor, ResourceProvider, ResourceSource, } from "./resourceProvider.js";
|
|
5
|
+
export { LocalizationProviderManager } from "./localizationProvider.js";
|
|
6
|
+
export type { LocalizationContext, LocalizationProvider, LocalizationProviderStats, LocalizationRequest, } from "./localizationProvider.js";
|
|
7
|
+
export { FontProviderManager } from "./fontProvider.js";
|
|
8
|
+
export type { FontContext, FontProvider, FontProviderStats, FontRequest, ProvidedFont, } from "./fontProvider.js";
|
|
9
|
+
export { createHttpPrebuiltSdfAtlasProvider, isValidPrebuiltSdfAtlasManifest, } from "./prebuiltSdfAtlas.js";
|
|
10
|
+
export type { PrebuiltSdfAtlasGlyph, PrebuiltSdfAtlasManifest, PrebuiltSdfAtlasPage, PrebuiltSdfAtlasProvider, } from "./prebuiltSdfAtlas.js";
|
|
11
|
+
export { createOriginPrebuiltSdfAtlasPackage, PrebuiltSdfAtlasStorageError, } from "./originPrebuiltSdfAtlasPackage.js";
|
|
12
|
+
export type { OriginPrebuiltSdfAtlasPackage, PrebuiltSdfAtlasInstallOptions, PrebuiltSdfAtlasInstallProgress, PrebuiltSdfAtlasPackageStatus, PrebuiltSdfAtlasStorageErrorCode, } from "./originPrebuiltSdfAtlasPackage.js";
|
|
13
|
+
export type { CoreControlBinding, CoreInteractionRegion, CoreSceneDelta, CoreSceneDump, StableId, } from "./types/core.js";
|
|
14
|
+
export type { RendererFontContract, RendererWorkerStats, } from "./protocol.js";
|
|
15
|
+
export type { RendererMasterData, } from "./worker-client.js";
|
|
16
|
+
export { RendererAuthoringDocument } from "./worker-client.js";
|
|
17
|
+
export { AUTHORING_CHECKPOINT_SCHEMA } from "./types/authoring.js";
|
|
18
|
+
export type { AuthoringCheckpoint, AuthoringCategory, AuthoringChangeKind, AuthoringCommand, AuthoringDelta, AuthoringElementChange, AuthoringSelection, AuthoringElementId, AuthoringPageChange, AuthoringPageChangeKind, GameProfileDocument, } from "./types/authoring.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { BrowserRenderer, BrowserRendererError, BrowserScene, } from "./renderer.js";
|
|
2
|
+
export { BrowserAuthoringClient } from "./authoring.js";
|
|
3
|
+
export { LocalizationProviderManager } from "./localizationProvider.js";
|
|
4
|
+
export { FontProviderManager } from "./fontProvider.js";
|
|
5
|
+
export { createHttpPrebuiltSdfAtlasProvider, isValidPrebuiltSdfAtlasManifest, } from "./prebuiltSdfAtlas.js";
|
|
6
|
+
export { createOriginPrebuiltSdfAtlasPackage, PrebuiltSdfAtlasStorageError, } from "./originPrebuiltSdfAtlasPackage.js";
|
|
7
|
+
export { RendererAuthoringDocument } from "./worker-client.js";
|
|
8
|
+
export { AUTHORING_CHECKPOINT_SCHEMA } from "./types/authoring.js";
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { GlyphInstance } from "../types/glyph.js";
|
|
2
|
+
type NumericRun = {
|
|
3
|
+
text: string;
|
|
4
|
+
plain_start: number;
|
|
5
|
+
plain_end: number;
|
|
6
|
+
};
|
|
7
|
+
type NumericCommand = {
|
|
8
|
+
id: string;
|
|
9
|
+
layer_id: string;
|
|
10
|
+
clip?: Array<[number, number]> | null;
|
|
11
|
+
numeric_text_runs?: NumericRun[];
|
|
12
|
+
};
|
|
13
|
+
export type NumericTextRegion = {
|
|
14
|
+
id: string;
|
|
15
|
+
layer_id: string;
|
|
16
|
+
role: "numeric_run";
|
|
17
|
+
bounds: {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
width: number;
|
|
21
|
+
height: number;
|
|
22
|
+
};
|
|
23
|
+
quad: [[number, number], [number, number], [number, number], [number, number]];
|
|
24
|
+
hit_geometry: [[number, number], [number, number], [number, number], [number, number]];
|
|
25
|
+
clip: Array<[number, number]> | null;
|
|
26
|
+
resolved_data: {
|
|
27
|
+
text: string;
|
|
28
|
+
plain_start: number;
|
|
29
|
+
plain_end: number;
|
|
30
|
+
};
|
|
31
|
+
capabilities: [];
|
|
32
|
+
render_mask: boolean;
|
|
33
|
+
};
|
|
34
|
+
export type NumericRegionRuntimeState = {
|
|
35
|
+
layerTransform: {
|
|
36
|
+
dx: number;
|
|
37
|
+
dy: number;
|
|
38
|
+
};
|
|
39
|
+
commandTransform: {
|
|
40
|
+
dx: number;
|
|
41
|
+
dy: number;
|
|
42
|
+
};
|
|
43
|
+
};
|
|
44
|
+
export declare function buildNumericTextRegions(command: NumericCommand, instances: Array<Pick<GlyphInstance, "layerId" | "plainTextIndex" | "deviceCharQuad">>, renderMask: boolean, runtimeState?: NumericRegionRuntimeState): NumericTextRegion[];
|
|
45
|
+
export declare function applyNumericRegionRuntimeState(region: NumericTextRegion, state: NumericRegionRuntimeState): NumericTextRegion;
|
|
46
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export function buildNumericTextRegions(command, instances, renderMask, runtimeState) {
|
|
2
|
+
return (command.numeric_text_runs ?? []).flatMap((run) => {
|
|
3
|
+
const points = instances
|
|
4
|
+
.filter((instance) => instance.layerId === command.id && instance.plainTextIndex >= run.plain_start && instance.plainTextIndex < run.plain_end)
|
|
5
|
+
.flatMap((instance) => instance.deviceCharQuad[1]);
|
|
6
|
+
if (points.length === 0)
|
|
7
|
+
return [];
|
|
8
|
+
const minX = Math.min(...points.map(([x]) => x));
|
|
9
|
+
const minY = Math.min(...points.map(([, y]) => y));
|
|
10
|
+
const maxX = Math.max(...points.map(([x]) => x));
|
|
11
|
+
const maxY = Math.max(...points.map(([, y]) => y));
|
|
12
|
+
const quad = [[minX, minY], [maxX, minY], [maxX, maxY], [minX, maxY]];
|
|
13
|
+
const region = {
|
|
14
|
+
id: `${command.id}:numeric:${run.plain_start}:${run.plain_end}`,
|
|
15
|
+
layer_id: command.layer_id,
|
|
16
|
+
role: "numeric_run",
|
|
17
|
+
bounds: { x: minX, y: minY, width: maxX - minX, height: maxY - minY },
|
|
18
|
+
quad,
|
|
19
|
+
hit_geometry: quad,
|
|
20
|
+
clip: command.clip ?? null,
|
|
21
|
+
resolved_data: { text: run.text, plain_start: run.plain_start, plain_end: run.plain_end },
|
|
22
|
+
capabilities: [],
|
|
23
|
+
render_mask: renderMask,
|
|
24
|
+
};
|
|
25
|
+
return [runtimeState ? applyNumericRegionRuntimeState(region, runtimeState) : region];
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
export function applyNumericRegionRuntimeState(region, state) {
|
|
29
|
+
const dx = state.layerTransform.dx + state.commandTransform.dx;
|
|
30
|
+
const dy = state.layerTransform.dy + state.commandTransform.dy;
|
|
31
|
+
const translate = ([x, y]) => [x + dx, y + dy];
|
|
32
|
+
const translateClip = ([x, y]) => [
|
|
33
|
+
x + state.layerTransform.dx,
|
|
34
|
+
y + state.layerTransform.dy,
|
|
35
|
+
];
|
|
36
|
+
const quad = region.quad.map(translate);
|
|
37
|
+
return {
|
|
38
|
+
...region,
|
|
39
|
+
bounds: { ...region.bounds, x: region.bounds.x + dx, y: region.bounds.y + dy },
|
|
40
|
+
quad,
|
|
41
|
+
hit_geometry: quad,
|
|
42
|
+
clip: region.clip?.map(translateClip) ?? null,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type LocalizationRequest = {
|
|
2
|
+
region: string;
|
|
3
|
+
locale: string;
|
|
4
|
+
key: string;
|
|
5
|
+
};
|
|
6
|
+
export type LocalizationContext = {
|
|
7
|
+
signal: AbortSignal;
|
|
8
|
+
};
|
|
9
|
+
/** Resolves renderer-owned UI text through arbitrary caller-owned logic. */
|
|
10
|
+
export interface LocalizationProvider {
|
|
11
|
+
provide(request: LocalizationRequest, context: LocalizationContext): Promise<string | null>;
|
|
12
|
+
}
|
|
13
|
+
export type LocalizationProviderManagerOptions = {
|
|
14
|
+
provider: LocalizationProvider;
|
|
15
|
+
concurrency?: number;
|
|
16
|
+
};
|
|
17
|
+
export type LocalizationProviderStats = {
|
|
18
|
+
requested: number;
|
|
19
|
+
unique: number;
|
|
20
|
+
resolved: number;
|
|
21
|
+
failures: number;
|
|
22
|
+
active: number;
|
|
23
|
+
peakActive: number;
|
|
24
|
+
resolveMs: number;
|
|
25
|
+
};
|
|
26
|
+
export declare class LocalizationProviderManager {
|
|
27
|
+
private readonly provider;
|
|
28
|
+
private readonly concurrency;
|
|
29
|
+
private readonly counters;
|
|
30
|
+
constructor(options: LocalizationProviderManagerOptions);
|
|
31
|
+
resolve(requests: readonly LocalizationRequest[], signal?: AbortSignal): Promise<Record<string, string>>;
|
|
32
|
+
stats(): LocalizationProviderStats;
|
|
33
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
export class LocalizationProviderManager {
|
|
2
|
+
provider;
|
|
3
|
+
concurrency;
|
|
4
|
+
counters = {
|
|
5
|
+
requested: 0,
|
|
6
|
+
unique: 0,
|
|
7
|
+
resolved: 0,
|
|
8
|
+
failures: 0,
|
|
9
|
+
active: 0,
|
|
10
|
+
peakActive: 0,
|
|
11
|
+
resolveMs: 0,
|
|
12
|
+
};
|
|
13
|
+
constructor(options) {
|
|
14
|
+
if (!options.provider || typeof options.provider.provide !== "function") {
|
|
15
|
+
throw new Error("localization provider is required");
|
|
16
|
+
}
|
|
17
|
+
const concurrency = options.concurrency ?? 8;
|
|
18
|
+
if (!Number.isInteger(concurrency) || concurrency <= 0) {
|
|
19
|
+
throw new Error("localization concurrency must be a positive integer");
|
|
20
|
+
}
|
|
21
|
+
this.provider = options.provider;
|
|
22
|
+
this.concurrency = concurrency;
|
|
23
|
+
}
|
|
24
|
+
async resolve(requests, signal = new AbortController().signal) {
|
|
25
|
+
const started = performance.now();
|
|
26
|
+
const unique = [...new Map(requests.map((request) => [
|
|
27
|
+
`${request.region}\0${request.locale}\0${request.key}`,
|
|
28
|
+
request,
|
|
29
|
+
])).values()];
|
|
30
|
+
this.counters.requested += requests.length;
|
|
31
|
+
this.counters.unique += unique.length;
|
|
32
|
+
const values = new Map();
|
|
33
|
+
let cursor = 0;
|
|
34
|
+
const worker = async () => {
|
|
35
|
+
while (cursor < unique.length) {
|
|
36
|
+
if (signal.aborted)
|
|
37
|
+
throw abortReason(signal);
|
|
38
|
+
const request = unique[cursor++];
|
|
39
|
+
this.counters.active += 1;
|
|
40
|
+
this.counters.peakActive = Math.max(this.counters.peakActive, this.counters.active);
|
|
41
|
+
let value;
|
|
42
|
+
try {
|
|
43
|
+
value = await this.provider.provide(request, { signal });
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
this.counters.failures += 1;
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
this.counters.active -= 1;
|
|
51
|
+
}
|
|
52
|
+
if (typeof value !== "string") {
|
|
53
|
+
this.counters.failures += 1;
|
|
54
|
+
throw new Error(`localization provider returned no text for ${request.locale}:${request.key}`);
|
|
55
|
+
}
|
|
56
|
+
this.counters.resolved += 1;
|
|
57
|
+
values.set(request.key, value);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
try {
|
|
61
|
+
await Promise.all(Array.from({ length: Math.min(this.concurrency, unique.length) }, worker));
|
|
62
|
+
return Object.fromEntries(values);
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
this.counters.resolveMs += performance.now() - started;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
stats() {
|
|
69
|
+
return { ...this.counters };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function abortReason(signal) {
|
|
73
|
+
return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
74
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type PrebuiltSdfAtlasProvider } from "./prebuiltSdfAtlas.js";
|
|
2
|
+
export type PrebuiltSdfAtlasInstallProgress = {
|
|
3
|
+
family: string;
|
|
4
|
+
completedPages: number;
|
|
5
|
+
totalPages: number;
|
|
6
|
+
storedBytes: number;
|
|
7
|
+
totalBytes: number;
|
|
8
|
+
};
|
|
9
|
+
export type PrebuiltSdfAtlasPackageStatus = {
|
|
10
|
+
namespace: string;
|
|
11
|
+
storageAvailable: boolean;
|
|
12
|
+
installedFamilies: string[];
|
|
13
|
+
installedPages: number;
|
|
14
|
+
storedBytes: number;
|
|
15
|
+
};
|
|
16
|
+
export type PrebuiltSdfAtlasInstallOptions = {
|
|
17
|
+
concurrency?: number;
|
|
18
|
+
onProgress?: (progress: PrebuiltSdfAtlasInstallProgress) => void;
|
|
19
|
+
requestPersistence?: boolean;
|
|
20
|
+
signal?: AbortSignal;
|
|
21
|
+
};
|
|
22
|
+
export type OriginPrebuiltSdfAtlasPackage = {
|
|
23
|
+
readonly namespace: string;
|
|
24
|
+
readonly provider: PrebuiltSdfAtlasProvider;
|
|
25
|
+
install(families: readonly string[], options?: PrebuiltSdfAtlasInstallOptions): Promise<PrebuiltSdfAtlasPackageStatus>;
|
|
26
|
+
remove(families?: readonly string[]): Promise<void>;
|
|
27
|
+
status(families: readonly string[]): Promise<PrebuiltSdfAtlasPackageStatus>;
|
|
28
|
+
close(): void;
|
|
29
|
+
};
|
|
30
|
+
export type PrebuiltSdfAtlasStorageErrorCode = "PREBUILT_ATLAS_STORAGE_UNAVAILABLE" | "PREBUILT_ATLAS_INVALID_MANIFEST" | "PREBUILT_ATLAS_QUOTA" | "PREBUILT_ATLAS_PAGE_HASH";
|
|
31
|
+
export declare class PrebuiltSdfAtlasStorageError extends Error {
|
|
32
|
+
readonly code: PrebuiltSdfAtlasStorageErrorCode;
|
|
33
|
+
constructor(code: PrebuiltSdfAtlasStorageErrorCode, message: string);
|
|
34
|
+
}
|
|
35
|
+
export declare function createOriginPrebuiltSdfAtlasPackage(options: {
|
|
36
|
+
namespace: string;
|
|
37
|
+
source: PrebuiltSdfAtlasProvider;
|
|
38
|
+
databaseName?: string;
|
|
39
|
+
}): OriginPrebuiltSdfAtlasPackage;
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { isValidPrebuiltSdfAtlasManifest, } from "./prebuiltSdfAtlas.js";
|
|
2
|
+
const DATABASE_NAME = "sekai-custom-profile-sdk-prebuilt-atlas-v1";
|
|
3
|
+
const DATABASE_VERSION = 1;
|
|
4
|
+
const ENTRY_STORE = "entries";
|
|
5
|
+
export class PrebuiltSdfAtlasStorageError extends Error {
|
|
6
|
+
code;
|
|
7
|
+
constructor(code, message) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.code = code;
|
|
10
|
+
this.name = "PrebuiltSdfAtlasStorageError";
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export function createOriginPrebuiltSdfAtlasPackage(options) {
|
|
14
|
+
if (!options.namespace.trim())
|
|
15
|
+
throw new TypeError("prebuilt atlas namespace must be non-empty");
|
|
16
|
+
const storage = new OriginPrebuiltAtlasStorage(options.databaseName ?? DATABASE_NAME);
|
|
17
|
+
const manifestCache = new Map();
|
|
18
|
+
const namespace = options.namespace.trim();
|
|
19
|
+
const installedManifest = async (family) => {
|
|
20
|
+
const cached = manifestCache.get(family);
|
|
21
|
+
if (cached)
|
|
22
|
+
return cached;
|
|
23
|
+
const record = await storage.getManifest(namespace, family);
|
|
24
|
+
if (!record || !isValidPrebuiltSdfAtlasManifest(record.manifest, family))
|
|
25
|
+
return null;
|
|
26
|
+
manifestCache.set(family, record.manifest);
|
|
27
|
+
return record.manifest;
|
|
28
|
+
};
|
|
29
|
+
const provider = {
|
|
30
|
+
async manifest(family, { signal }) {
|
|
31
|
+
if (signal.aborted)
|
|
32
|
+
throw abortReason(signal);
|
|
33
|
+
if (!storage.available)
|
|
34
|
+
return null;
|
|
35
|
+
try {
|
|
36
|
+
return await installedManifest(family);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
async page(family, file, { signal }) {
|
|
43
|
+
if (signal.aborted)
|
|
44
|
+
throw abortReason(signal);
|
|
45
|
+
const manifest = await installedManifest(family);
|
|
46
|
+
const descriptor = manifest?.pages.find((page) => page.file === file);
|
|
47
|
+
if (!descriptor)
|
|
48
|
+
throw new Error(`prebuilt atlas page is not installed: ${family}/${file}`);
|
|
49
|
+
const record = await storage.getPage(namespace, family, file, descriptor.file_sha256);
|
|
50
|
+
if (!record) {
|
|
51
|
+
manifestCache.delete(family);
|
|
52
|
+
await storage.deleteManifest(namespace, family).catch(() => undefined);
|
|
53
|
+
throw new Error(`prebuilt atlas page is missing: ${family}/${file}`);
|
|
54
|
+
}
|
|
55
|
+
return record.bytes.slice(0);
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
return {
|
|
59
|
+
namespace,
|
|
60
|
+
provider,
|
|
61
|
+
async status(families) {
|
|
62
|
+
if (!storage.available)
|
|
63
|
+
return emptyStatus(namespace, false);
|
|
64
|
+
const records = await Promise.all(uniqueFamilies(families).map((family) => storage.getManifest(namespace, family)));
|
|
65
|
+
return records.reduce((status, record) => {
|
|
66
|
+
if (!record || !isValidPrebuiltSdfAtlasManifest(record.manifest, record.family))
|
|
67
|
+
return status;
|
|
68
|
+
status.installedFamilies.push(record.family);
|
|
69
|
+
status.installedPages += record.installedPages;
|
|
70
|
+
status.storedBytes += record.storedBytes;
|
|
71
|
+
return status;
|
|
72
|
+
}, emptyStatus(namespace, true));
|
|
73
|
+
},
|
|
74
|
+
async install(families, installOptions = {}) {
|
|
75
|
+
if (!storage.available) {
|
|
76
|
+
throw new PrebuiltSdfAtlasStorageError("PREBUILT_ATLAS_STORAGE_UNAVAILABLE", "IndexedDB is unavailable for the prebuilt atlas package");
|
|
77
|
+
}
|
|
78
|
+
const signal = installOptions.signal ?? new AbortController().signal;
|
|
79
|
+
const requested = uniqueFamilies(families);
|
|
80
|
+
const current = await this.status(requested);
|
|
81
|
+
const installed = new Set(current.installedFamilies);
|
|
82
|
+
const missing = requested.filter((family) => !installed.has(family));
|
|
83
|
+
if (missing.length === 0)
|
|
84
|
+
return current;
|
|
85
|
+
const manifests = new Map();
|
|
86
|
+
for (const family of missing) {
|
|
87
|
+
if (signal.aborted)
|
|
88
|
+
throw abortReason(signal);
|
|
89
|
+
const manifest = await options.source.manifest(family, { signal });
|
|
90
|
+
if (!manifest || !isValidPrebuiltSdfAtlasManifest(manifest, family)) {
|
|
91
|
+
throw new PrebuiltSdfAtlasStorageError("PREBUILT_ATLAS_INVALID_MANIFEST", `prebuilt atlas manifest is invalid: ${family}`);
|
|
92
|
+
}
|
|
93
|
+
manifests.set(family, manifest);
|
|
94
|
+
}
|
|
95
|
+
const totalPages = [...manifests.values()].reduce((sum, manifest) => sum + manifest.pages.length, 0);
|
|
96
|
+
const totalBytes = [...manifests.values()].flatMap((manifest) => manifest.pages)
|
|
97
|
+
.reduce((sum, page) => sum + 64 + page.width * page.height, 0);
|
|
98
|
+
await assertStorageQuota(totalBytes);
|
|
99
|
+
if (installOptions.requestPersistence)
|
|
100
|
+
await requestOriginPersistence();
|
|
101
|
+
await storage.remove(namespace, missing);
|
|
102
|
+
manifestCache.clear();
|
|
103
|
+
const tasks = [...manifests].flatMap(([family, manifest]) => manifest.pages.map((page) => ({ family, page })));
|
|
104
|
+
const concurrency = boundedConcurrency(installOptions.concurrency ?? 4);
|
|
105
|
+
let cursor = 0;
|
|
106
|
+
let completedPages = 0;
|
|
107
|
+
let storedBytes = 0;
|
|
108
|
+
const familyBytes = new Map();
|
|
109
|
+
try {
|
|
110
|
+
await Promise.all(Array.from({ length: Math.min(concurrency, tasks.length) }, async () => {
|
|
111
|
+
while (cursor < tasks.length) {
|
|
112
|
+
const task = tasks[cursor++];
|
|
113
|
+
if (signal.aborted)
|
|
114
|
+
throw abortReason(signal);
|
|
115
|
+
const buffer = await options.source.page(task.family, task.page.file, { signal });
|
|
116
|
+
await assertPageHash(buffer, task.page.file_sha256, task.family, task.page.file);
|
|
117
|
+
await storage.putPage({
|
|
118
|
+
key: pageKey(namespace, task.family, task.page.file, task.page.file_sha256),
|
|
119
|
+
kind: "page",
|
|
120
|
+
namespace,
|
|
121
|
+
family: task.family,
|
|
122
|
+
file: task.page.file,
|
|
123
|
+
sha256: task.page.file_sha256,
|
|
124
|
+
bytes: buffer,
|
|
125
|
+
});
|
|
126
|
+
completedPages += 1;
|
|
127
|
+
storedBytes += buffer.byteLength;
|
|
128
|
+
familyBytes.set(task.family, (familyBytes.get(task.family) ?? 0) + buffer.byteLength);
|
|
129
|
+
installOptions.onProgress?.({
|
|
130
|
+
family: task.family,
|
|
131
|
+
completedPages,
|
|
132
|
+
totalPages,
|
|
133
|
+
storedBytes,
|
|
134
|
+
totalBytes,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}));
|
|
138
|
+
await storage.putManifests([...manifests].map(([family, manifest]) => ({
|
|
139
|
+
key: manifestKey(namespace, family),
|
|
140
|
+
kind: "manifest",
|
|
141
|
+
namespace,
|
|
142
|
+
family,
|
|
143
|
+
manifest,
|
|
144
|
+
installedPages: manifest.pages.length,
|
|
145
|
+
storedBytes: familyBytes.get(family) ?? 0,
|
|
146
|
+
installedAtMs: Date.now(),
|
|
147
|
+
})));
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
await storage.remove(namespace, missing).catch(() => undefined);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
return this.status(requested);
|
|
154
|
+
},
|
|
155
|
+
async remove(families) {
|
|
156
|
+
manifestCache.clear();
|
|
157
|
+
if (!storage.available)
|
|
158
|
+
return;
|
|
159
|
+
await storage.remove(namespace, families == null ? undefined : uniqueFamilies(families));
|
|
160
|
+
},
|
|
161
|
+
close() {
|
|
162
|
+
manifestCache.clear();
|
|
163
|
+
storage.close();
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
class OriginPrebuiltAtlasStorage {
|
|
168
|
+
databaseName;
|
|
169
|
+
databasePromise = null;
|
|
170
|
+
constructor(databaseName) {
|
|
171
|
+
this.databaseName = databaseName;
|
|
172
|
+
}
|
|
173
|
+
get available() { return typeof indexedDB !== "undefined"; }
|
|
174
|
+
async getManifest(namespace, family) {
|
|
175
|
+
const record = await this.get(manifestKey(namespace, family));
|
|
176
|
+
return record?.kind === "manifest" ? record : null;
|
|
177
|
+
}
|
|
178
|
+
async getPage(namespace, family, file, sha256) {
|
|
179
|
+
const record = await this.get(pageKey(namespace, family, file, sha256));
|
|
180
|
+
return record?.kind === "page" ? record : null;
|
|
181
|
+
}
|
|
182
|
+
async putPage(record) {
|
|
183
|
+
const database = await this.database();
|
|
184
|
+
const transaction = database.transaction(ENTRY_STORE, "readwrite", { durability: "relaxed" });
|
|
185
|
+
transaction.objectStore(ENTRY_STORE).put(record);
|
|
186
|
+
await transactionDone(transaction);
|
|
187
|
+
}
|
|
188
|
+
async putManifests(records) {
|
|
189
|
+
const database = await this.database();
|
|
190
|
+
const transaction = database.transaction(ENTRY_STORE, "readwrite");
|
|
191
|
+
const store = transaction.objectStore(ENTRY_STORE);
|
|
192
|
+
for (const record of records)
|
|
193
|
+
store.put(record);
|
|
194
|
+
await transactionDone(transaction);
|
|
195
|
+
}
|
|
196
|
+
async deleteManifest(namespace, family) {
|
|
197
|
+
const database = await this.database();
|
|
198
|
+
const transaction = database.transaction(ENTRY_STORE, "readwrite");
|
|
199
|
+
transaction.objectStore(ENTRY_STORE).delete(manifestKey(namespace, family));
|
|
200
|
+
await transactionDone(transaction);
|
|
201
|
+
}
|
|
202
|
+
async remove(namespace, families) {
|
|
203
|
+
const database = await this.database();
|
|
204
|
+
const transaction = database.transaction(ENTRY_STORE, "readwrite");
|
|
205
|
+
const store = transaction.objectStore(ENTRY_STORE);
|
|
206
|
+
const prefixes = (families?.length ? families : [""]).map((family) => familyPrefix(namespace, family));
|
|
207
|
+
await deletePrefixes(store, prefixes);
|
|
208
|
+
await transactionDone(transaction);
|
|
209
|
+
}
|
|
210
|
+
close() {
|
|
211
|
+
void this.databasePromise?.then((database) => database.close());
|
|
212
|
+
this.databasePromise = null;
|
|
213
|
+
}
|
|
214
|
+
async get(key) {
|
|
215
|
+
const database = await this.database();
|
|
216
|
+
const transaction = database.transaction(ENTRY_STORE, "readonly");
|
|
217
|
+
const record = await request(transaction.objectStore(ENTRY_STORE).get(key));
|
|
218
|
+
await transactionDone(transaction);
|
|
219
|
+
return record ?? null;
|
|
220
|
+
}
|
|
221
|
+
database() {
|
|
222
|
+
if (!this.available)
|
|
223
|
+
return Promise.reject(new Error("IndexedDB is unavailable"));
|
|
224
|
+
this.databasePromise ??= new Promise((resolve, reject) => {
|
|
225
|
+
const open = indexedDB.open(this.databaseName, DATABASE_VERSION);
|
|
226
|
+
open.onupgradeneeded = () => {
|
|
227
|
+
if (!open.result.objectStoreNames.contains(ENTRY_STORE)) {
|
|
228
|
+
open.result.createObjectStore(ENTRY_STORE, { keyPath: "key" });
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
open.onerror = () => reject(open.error ?? new Error("open IndexedDB prebuilt atlas storage failed"));
|
|
232
|
+
open.onblocked = () => reject(new Error("IndexedDB prebuilt atlas storage upgrade was blocked"));
|
|
233
|
+
open.onsuccess = () => {
|
|
234
|
+
open.result.onversionchange = () => open.result.close();
|
|
235
|
+
resolve(open.result);
|
|
236
|
+
};
|
|
237
|
+
});
|
|
238
|
+
return this.databasePromise;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
function emptyStatus(namespace, storageAvailable) {
|
|
242
|
+
return { namespace, storageAvailable, installedFamilies: [], installedPages: 0, storedBytes: 0 };
|
|
243
|
+
}
|
|
244
|
+
function uniqueFamilies(families) {
|
|
245
|
+
return [...new Set(families.map((family) => family.trim()).filter(Boolean))];
|
|
246
|
+
}
|
|
247
|
+
function familyPrefix(namespace, family) {
|
|
248
|
+
return family ? `${namespace}\0${family}\0` : `${namespace}\0`;
|
|
249
|
+
}
|
|
250
|
+
function manifestKey(namespace, family) {
|
|
251
|
+
return `${familyPrefix(namespace, family)}manifest`;
|
|
252
|
+
}
|
|
253
|
+
function pageKey(namespace, family, file, sha256) {
|
|
254
|
+
return `${familyPrefix(namespace, family)}page\0${file}\0${sha256}`;
|
|
255
|
+
}
|
|
256
|
+
function boundedConcurrency(value) {
|
|
257
|
+
if (!Number.isInteger(value) || value <= 0)
|
|
258
|
+
throw new TypeError("prebuilt atlas install concurrency must be positive");
|
|
259
|
+
return Math.min(8, value);
|
|
260
|
+
}
|
|
261
|
+
async function assertStorageQuota(requiredBytes) {
|
|
262
|
+
if (typeof navigator === "undefined" || !navigator.storage?.estimate)
|
|
263
|
+
return;
|
|
264
|
+
const estimate = await navigator.storage.estimate();
|
|
265
|
+
if (estimate.quota == null || estimate.usage == null)
|
|
266
|
+
return;
|
|
267
|
+
const available = Math.max(0, estimate.quota - estimate.usage);
|
|
268
|
+
if (requiredBytes > available) {
|
|
269
|
+
throw new PrebuiltSdfAtlasStorageError("PREBUILT_ATLAS_QUOTA", `prebuilt atlas requires ${requiredBytes} bytes but only ${available} bytes are available`);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
async function requestOriginPersistence() {
|
|
273
|
+
if (typeof navigator === "undefined" || !navigator.storage?.persist)
|
|
274
|
+
return;
|
|
275
|
+
await navigator.storage.persist();
|
|
276
|
+
}
|
|
277
|
+
async function assertPageHash(buffer, expected, family, file) {
|
|
278
|
+
const digest = new Uint8Array(await crypto.subtle.digest("SHA-256", buffer.slice(0)));
|
|
279
|
+
const actual = [...digest].map((value) => value.toString(16).padStart(2, "0")).join("");
|
|
280
|
+
if (actual !== expected.toLowerCase()) {
|
|
281
|
+
throw new PrebuiltSdfAtlasStorageError("PREBUILT_ATLAS_PAGE_HASH", `prebuilt atlas page hash mismatch: ${family}/${file}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
function abortReason(signal) {
|
|
285
|
+
return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
286
|
+
}
|
|
287
|
+
function request(value) {
|
|
288
|
+
return new Promise((resolve, reject) => {
|
|
289
|
+
value.onsuccess = () => resolve(value.result);
|
|
290
|
+
value.onerror = () => reject(value.error ?? new Error("IndexedDB prebuilt atlas request failed"));
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
function transactionDone(transaction) {
|
|
294
|
+
return new Promise((resolve, reject) => {
|
|
295
|
+
transaction.oncomplete = () => resolve();
|
|
296
|
+
transaction.onerror = () => reject(transaction.error ?? new Error("IndexedDB prebuilt atlas transaction failed"));
|
|
297
|
+
transaction.onabort = () => reject(transaction.error ?? new Error("IndexedDB prebuilt atlas transaction aborted"));
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
function deletePrefixes(store, prefixes) {
|
|
301
|
+
return new Promise((resolve, reject) => {
|
|
302
|
+
const cursor = store.openKeyCursor();
|
|
303
|
+
cursor.onerror = () => reject(cursor.error ?? new Error("IndexedDB prebuilt atlas cursor failed"));
|
|
304
|
+
cursor.onsuccess = () => {
|
|
305
|
+
const value = cursor.result;
|
|
306
|
+
if (!value) {
|
|
307
|
+
resolve();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const key = String(value.primaryKey);
|
|
311
|
+
if (prefixes.some((prefix) => key.startsWith(prefix)))
|
|
312
|
+
store.delete(value.primaryKey);
|
|
313
|
+
value.continue();
|
|
314
|
+
};
|
|
315
|
+
});
|
|
316
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type GlyphRequest, type SdfAtlas } from "./fontSdfAtlas.js";
|
|
2
|
+
export type PrebuiltSdfAtlasPage = {
|
|
3
|
+
file: string;
|
|
4
|
+
width: number;
|
|
5
|
+
height: number;
|
|
6
|
+
file_sha256: string;
|
|
7
|
+
};
|
|
8
|
+
export type PrebuiltSdfAtlasGlyph = {
|
|
9
|
+
codepoint: number;
|
|
10
|
+
page: number;
|
|
11
|
+
rect: [number, number, number, number];
|
|
12
|
+
plane_bearing: [number, number];
|
|
13
|
+
plane_size: [number, number];
|
|
14
|
+
plane_advance_x: number;
|
|
15
|
+
};
|
|
16
|
+
export type PrebuiltSdfAtlasManifest = {
|
|
17
|
+
schema: "allium.sdf-atlas-manifest.v1";
|
|
18
|
+
generator_contract: string;
|
|
19
|
+
font_family: string;
|
|
20
|
+
font_sha256: string;
|
|
21
|
+
point_size: number;
|
|
22
|
+
spread: number;
|
|
23
|
+
pages: PrebuiltSdfAtlasPage[];
|
|
24
|
+
glyphs: PrebuiltSdfAtlasGlyph[];
|
|
25
|
+
};
|
|
26
|
+
export interface PrebuiltSdfAtlasProvider {
|
|
27
|
+
manifest(family: string, context: {
|
|
28
|
+
signal: AbortSignal;
|
|
29
|
+
}): Promise<PrebuiltSdfAtlasManifest | null>;
|
|
30
|
+
page(family: string, file: string, context: {
|
|
31
|
+
signal: AbortSignal;
|
|
32
|
+
}): Promise<ArrayBuffer>;
|
|
33
|
+
}
|
|
34
|
+
export declare function createHttpPrebuiltSdfAtlasProvider(baseUrl: string): PrebuiltSdfAtlasProvider;
|
|
35
|
+
export declare function buildPrebuiltSdfAtlas(provider: PrebuiltSdfAtlasProvider, requests: GlyphRequest[], signal: AbortSignal, maxPages?: number): Promise<SdfAtlas | null>;
|
|
36
|
+
export declare function isValidPrebuiltSdfAtlasManifest(manifest: PrebuiltSdfAtlasManifest, family: string): boolean;
|