@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
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
use freetype::{face::LoadFlag, Library};
|
|
2
|
+
use serde::Serialize;
|
|
3
|
+
use serde_json::json;
|
|
4
|
+
use sha2::{Digest, Sha256};
|
|
5
|
+
use std::rc::Rc;
|
|
6
|
+
|
|
7
|
+
use super::{atlas::AtlasConfig, FONT_ENGINE_FINGERPRINT, TMP_POINT_SIZE, TMP_SPREAD};
|
|
8
|
+
|
|
9
|
+
const CACHE_SCHEMA: &str = "allium.glyph-raster-cache.v1";
|
|
10
|
+
const THRESHOLD: u32 = 128;
|
|
11
|
+
const DOWNSAMPLE_VERSION: &str = "box-v1";
|
|
12
|
+
|
|
13
|
+
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
|
|
14
|
+
#[serde(rename_all = "lowercase")]
|
|
15
|
+
pub enum RasterBackend {
|
|
16
|
+
Edt,
|
|
17
|
+
Analytic,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
impl RasterBackend {
|
|
21
|
+
pub fn parse(value: &str) -> Result<Self, String> {
|
|
22
|
+
match value {
|
|
23
|
+
"edt" => Ok(Self::Edt),
|
|
24
|
+
"analytic" => Ok(Self::Analytic),
|
|
25
|
+
_ => Err(format!("unsupported SDF backend {value}")),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
fn load_flags(self) -> &'static str {
|
|
30
|
+
match self {
|
|
31
|
+
Self::Edt => "NO_HINTING",
|
|
32
|
+
Self::Analytic => "NO_BITMAP|NO_HINTING",
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
fn render_mode(self) -> &'static str {
|
|
37
|
+
match self {
|
|
38
|
+
Self::Edt => "normal-mask",
|
|
39
|
+
Self::Analytic => "outline",
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
fn normalized_supersample(self, requested: usize) -> usize {
|
|
44
|
+
match self {
|
|
45
|
+
Self::Edt => requested.clamp(1, 4),
|
|
46
|
+
Self::Analytic => 0,
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
fn load_flag(self) -> LoadFlag {
|
|
51
|
+
match self {
|
|
52
|
+
Self::Edt => LoadFlag::NO_HINTING,
|
|
53
|
+
Self::Analytic => LoadFlag::NO_BITMAP | LoadFlag::NO_HINTING,
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
fn name(self) -> &'static str {
|
|
58
|
+
match self {
|
|
59
|
+
Self::Edt => "edt",
|
|
60
|
+
Self::Analytic => "analytic",
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
#[derive(Serialize)]
|
|
66
|
+
#[serde(rename_all = "camelCase")]
|
|
67
|
+
pub struct GlyphRasterPlan {
|
|
68
|
+
region: String,
|
|
69
|
+
family: String,
|
|
70
|
+
font_source_hash: String,
|
|
71
|
+
schema_namespace: &'static str,
|
|
72
|
+
font_engine_fingerprint: &'static str,
|
|
73
|
+
raster_contract_id: String,
|
|
74
|
+
contract_id: String,
|
|
75
|
+
backend: RasterBackend,
|
|
76
|
+
supersample: usize,
|
|
77
|
+
base_size: f32,
|
|
78
|
+
spread: f32,
|
|
79
|
+
atlas_width: usize,
|
|
80
|
+
atlas_height: usize,
|
|
81
|
+
glyphs: Vec<PlannedGlyph>,
|
|
82
|
+
missing: Vec<String>,
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
#[derive(Serialize)]
|
|
86
|
+
#[serde(rename_all = "camelCase")]
|
|
87
|
+
struct PlannedGlyph {
|
|
88
|
+
ch: String,
|
|
89
|
+
glyph_index: u32,
|
|
90
|
+
identity: GlyphRasterIdentity,
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[derive(Serialize)]
|
|
94
|
+
#[serde(rename_all = "camelCase")]
|
|
95
|
+
struct GlyphRasterIdentity {
|
|
96
|
+
opaque_key: String,
|
|
97
|
+
schema_namespace: &'static str,
|
|
98
|
+
font_engine_fingerprint: &'static str,
|
|
99
|
+
raster_contract_id: String,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
pub fn plan(
|
|
103
|
+
font_bytes: Vec<u8>,
|
|
104
|
+
codepoints: &[u32],
|
|
105
|
+
region: String,
|
|
106
|
+
family: String,
|
|
107
|
+
font_source_hash: String,
|
|
108
|
+
backend: RasterBackend,
|
|
109
|
+
requested_supersample: usize,
|
|
110
|
+
) -> Result<GlyphRasterPlan, String> {
|
|
111
|
+
let supersample = backend.normalized_supersample(requested_supersample);
|
|
112
|
+
let raster_contract_id = format!(
|
|
113
|
+
"allium-r8-{}-ss{supersample}-spread{}-threshold{THRESHOLD}-{DOWNSAMPLE_VERSION}",
|
|
114
|
+
backend.name(),
|
|
115
|
+
TMP_SPREAD as usize,
|
|
116
|
+
);
|
|
117
|
+
let contract_id = format!("{FONT_ENGINE_FINGERPRINT}:{raster_contract_id}");
|
|
118
|
+
let library = Library::init().map_err(|error| format!("FreeType init failed: {error:?}"))?;
|
|
119
|
+
let face = library
|
|
120
|
+
.new_memory_face(Rc::new(font_bytes), 0)
|
|
121
|
+
.map_err(|error| format!("load memory face failed: {error:?}"))?;
|
|
122
|
+
face.set_char_size((TMP_POINT_SIZE as isize) * 64, 0, 72, 72)
|
|
123
|
+
.map_err(|error| format!("set char size failed: {error:?}"))?;
|
|
124
|
+
let mut glyphs = Vec::with_capacity(codepoints.len());
|
|
125
|
+
let mut missing = Vec::new();
|
|
126
|
+
for codepoint in codepoints {
|
|
127
|
+
let Some(ch) = char::from_u32(*codepoint) else {
|
|
128
|
+
missing.push(format!("U+{codepoint:04X}"));
|
|
129
|
+
continue;
|
|
130
|
+
};
|
|
131
|
+
let Some(glyph_index) = face.get_char_index(ch as usize) else {
|
|
132
|
+
missing.push(ch.to_string());
|
|
133
|
+
continue;
|
|
134
|
+
};
|
|
135
|
+
face.load_glyph(glyph_index, backend.load_flag())
|
|
136
|
+
.map_err(|error| format!("load glyph plan failed: {error:?}"))?;
|
|
137
|
+
glyphs.push(PlannedGlyph {
|
|
138
|
+
ch: ch.to_string(),
|
|
139
|
+
glyph_index,
|
|
140
|
+
identity: identity(
|
|
141
|
+
®ion,
|
|
142
|
+
&font_source_hash,
|
|
143
|
+
glyph_index,
|
|
144
|
+
backend,
|
|
145
|
+
supersample,
|
|
146
|
+
&raster_contract_id,
|
|
147
|
+
)?,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
let atlas = AtlasConfig::default();
|
|
151
|
+
Ok(GlyphRasterPlan {
|
|
152
|
+
region,
|
|
153
|
+
family,
|
|
154
|
+
font_source_hash,
|
|
155
|
+
schema_namespace: CACHE_SCHEMA,
|
|
156
|
+
font_engine_fingerprint: FONT_ENGINE_FINGERPRINT,
|
|
157
|
+
raster_contract_id,
|
|
158
|
+
contract_id,
|
|
159
|
+
backend,
|
|
160
|
+
supersample,
|
|
161
|
+
base_size: TMP_POINT_SIZE,
|
|
162
|
+
spread: TMP_SPREAD,
|
|
163
|
+
atlas_width: atlas.page_width,
|
|
164
|
+
atlas_height: atlas.page_height,
|
|
165
|
+
glyphs,
|
|
166
|
+
missing,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
fn identity(
|
|
171
|
+
region: &str,
|
|
172
|
+
font_source_hash: &str,
|
|
173
|
+
glyph_index: u32,
|
|
174
|
+
backend: RasterBackend,
|
|
175
|
+
supersample: usize,
|
|
176
|
+
raster_contract_id: &str,
|
|
177
|
+
) -> Result<GlyphRasterIdentity, String> {
|
|
178
|
+
let canonical = json!([
|
|
179
|
+
["schema", CACHE_SCHEMA],
|
|
180
|
+
["region", region],
|
|
181
|
+
["font_sha256", font_source_hash.to_ascii_lowercase()],
|
|
182
|
+
["face_index", 0],
|
|
183
|
+
["variation_axes", json!([])],
|
|
184
|
+
["glyph_id", glyph_index],
|
|
185
|
+
["point_size_26d6", (TMP_POINT_SIZE as u32) * 64],
|
|
186
|
+
["dpi_x", 72],
|
|
187
|
+
["dpi_y", 72],
|
|
188
|
+
["load_flags", backend.load_flags()],
|
|
189
|
+
["render_mode", backend.render_mode()],
|
|
190
|
+
["spread_26d6", (TMP_SPREAD as u32) * 64],
|
|
191
|
+
["sdf_algorithm", backend.name()],
|
|
192
|
+
["supersample", supersample],
|
|
193
|
+
["threshold", THRESHOLD],
|
|
194
|
+
["downsample_version", DOWNSAMPLE_VERSION],
|
|
195
|
+
["font_engine", FONT_ENGINE_FINGERPRINT],
|
|
196
|
+
["raster_contract", raster_contract_id],
|
|
197
|
+
]);
|
|
198
|
+
let bytes = serde_json::to_vec(&canonical).map_err(|error| error.to_string())?;
|
|
199
|
+
Ok(GlyphRasterIdentity {
|
|
200
|
+
opaque_key: format!("{:x}", Sha256::digest(bytes)),
|
|
201
|
+
schema_namespace: CACHE_SCHEMA,
|
|
202
|
+
font_engine_fingerprint: FONT_ENGINE_FINGERPRINT,
|
|
203
|
+
raster_contract_id: raster_contract_id.to_string(),
|
|
204
|
+
})
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
#[cfg(test)]
|
|
208
|
+
mod tests {
|
|
209
|
+
use super::*;
|
|
210
|
+
|
|
211
|
+
#[test]
|
|
212
|
+
fn raster_contract_normalizes_supersample_before_identity() {
|
|
213
|
+
assert_eq!(RasterBackend::Edt.normalized_supersample(99), 4);
|
|
214
|
+
assert_eq!(RasterBackend::Analytic.normalized_supersample(99), 0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
#[test]
|
|
218
|
+
fn identity_matches_the_existing_browser_cache_namespace() {
|
|
219
|
+
let contract = "allium-r8-edt-ss2-spread6-threshold128-box-v1";
|
|
220
|
+
let planned = identity("cn", &"a".repeat(64), 42, RasterBackend::Edt, 2, contract).unwrap();
|
|
221
|
+
assert_eq!(
|
|
222
|
+
planned.opaque_key,
|
|
223
|
+
"fbd1658fe3bd464c0a54ee42284e7c15f64b68dd60d270c81e818de61bac9894"
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { SessionImageResourceCache, type ImageResourceLease } from "../cache/sessionImageResourceCache.js";
|
|
2
|
+
import type {
|
|
3
|
+
ProvidedResource,
|
|
4
|
+
ResourceDescriptor,
|
|
5
|
+
ResourceProvider,
|
|
6
|
+
} from "../resourceProvider.js";
|
|
7
|
+
|
|
8
|
+
export type BrowserImageSource = {
|
|
9
|
+
source: TexImageSource;
|
|
10
|
+
width: number;
|
|
11
|
+
height: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export type BrowserSemanticResourceOptions = {
|
|
15
|
+
provider: ResourceProvider;
|
|
16
|
+
concurrency?: number;
|
|
17
|
+
decode?: (blob: Blob) => Promise<TexImageSource>;
|
|
18
|
+
softBytes?: number;
|
|
19
|
+
hardBytes?: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type BrowserSemanticResourceSet = {
|
|
23
|
+
sources: Map<string, BrowserImageSource>;
|
|
24
|
+
availability: Map<string, boolean>;
|
|
25
|
+
release(): void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export type ResourceProviderRuntimeStats = {
|
|
29
|
+
requested: number;
|
|
30
|
+
loaded: number;
|
|
31
|
+
failures: number;
|
|
32
|
+
cancellations: number;
|
|
33
|
+
encodedBytes: number;
|
|
34
|
+
resolveMs: number;
|
|
35
|
+
active: number;
|
|
36
|
+
queued: number;
|
|
37
|
+
limit: number;
|
|
38
|
+
peak: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type LoadedBrowserResource = {
|
|
42
|
+
image: BrowserImageSource;
|
|
43
|
+
available: boolean;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** The provider owns acquisition and persistent caching. This manager only
|
|
47
|
+
* schedules provider calls and retains bounded decoded session resources. */
|
|
48
|
+
export class BrowserSemanticResourceManager {
|
|
49
|
+
private readonly provider: ResourceProvider;
|
|
50
|
+
private readonly decode: (blob: Blob) => Promise<TexImageSource>;
|
|
51
|
+
private readonly scheduler: BoundedScheduler;
|
|
52
|
+
private readonly cache: SessionImageResourceCache<LoadedBrowserResource>;
|
|
53
|
+
private readonly providerCounters = {
|
|
54
|
+
requested: 0,
|
|
55
|
+
loaded: 0,
|
|
56
|
+
failures: 0,
|
|
57
|
+
cancellations: 0,
|
|
58
|
+
encodedBytes: 0,
|
|
59
|
+
resolveMs: 0,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
constructor(options: BrowserSemanticResourceOptions) {
|
|
63
|
+
if (!options.provider || typeof options.provider.provide !== "function") {
|
|
64
|
+
throw new Error("semantic resource provider is required");
|
|
65
|
+
}
|
|
66
|
+
this.provider = options.provider;
|
|
67
|
+
this.decode = options.decode ?? ((blob: Blob) => createImageBitmap(blob, {
|
|
68
|
+
premultiplyAlpha: "none",
|
|
69
|
+
colorSpaceConversion: "none",
|
|
70
|
+
}) as Promise<TexImageSource>);
|
|
71
|
+
this.scheduler = new BoundedScheduler(options.concurrency ?? 8);
|
|
72
|
+
this.cache = new SessionImageResourceCache<LoadedBrowserResource>({
|
|
73
|
+
softBytes: options.softBytes ?? 64 * 1024 * 1024,
|
|
74
|
+
hardBytes: options.hardBytes ?? 96 * 1024 * 1024,
|
|
75
|
+
dispose: (value) => closeImageSource(value.image.source),
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async acquire(
|
|
80
|
+
resources: ResourceDescriptor[],
|
|
81
|
+
signal: AbortSignal = new AbortController().signal,
|
|
82
|
+
): Promise<BrowserSemanticResourceSet> {
|
|
83
|
+
const unique = new Map(resources.map((resource) => [resourceIdentity(resource), resource] as const));
|
|
84
|
+
const leases: Array<{
|
|
85
|
+
identity: string;
|
|
86
|
+
lease: ImageResourceLease<LoadedBrowserResource>;
|
|
87
|
+
}> = [];
|
|
88
|
+
try {
|
|
89
|
+
await Promise.all([...unique].map(async ([identity, resource]) => {
|
|
90
|
+
const cacheIdentity = this.provider.cacheIdentity?.(resource) ?? identity;
|
|
91
|
+
const lease = await this.cache.acquire(cacheIdentity, async (sharedSignal) => {
|
|
92
|
+
const value = await this.load(resource, sharedSignal);
|
|
93
|
+
return { value, bytes: decodedBytes(value.image) };
|
|
94
|
+
}, signal);
|
|
95
|
+
leases.push({ identity, lease });
|
|
96
|
+
}));
|
|
97
|
+
} catch (error) {
|
|
98
|
+
for (const { lease } of leases) lease.release();
|
|
99
|
+
throw error;
|
|
100
|
+
}
|
|
101
|
+
const sources = new Map(
|
|
102
|
+
leases.map(({ identity, lease }) => [identity, lease.value.image] as const),
|
|
103
|
+
);
|
|
104
|
+
const availability = new Map(
|
|
105
|
+
leases.map(({ identity, lease }) => [identity, lease.value.available] as const),
|
|
106
|
+
);
|
|
107
|
+
let released = false;
|
|
108
|
+
return {
|
|
109
|
+
sources,
|
|
110
|
+
availability,
|
|
111
|
+
release: () => {
|
|
112
|
+
if (released) return;
|
|
113
|
+
released = true;
|
|
114
|
+
for (const { lease } of leases) lease.release();
|
|
115
|
+
},
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
stats() {
|
|
120
|
+
return {
|
|
121
|
+
...this.cache.stats(),
|
|
122
|
+
provider: {
|
|
123
|
+
...this.providerCounters,
|
|
124
|
+
...this.scheduler.stats(),
|
|
125
|
+
} satisfies ResourceProviderRuntimeStats,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
private async load(
|
|
130
|
+
resource: ResourceDescriptor,
|
|
131
|
+
signal: AbortSignal,
|
|
132
|
+
): Promise<LoadedBrowserResource> {
|
|
133
|
+
this.providerCounters.requested += 1;
|
|
134
|
+
const started = performance.now();
|
|
135
|
+
try {
|
|
136
|
+
const provided = await this.scheduler.run(
|
|
137
|
+
() => this.provider.provide(resource, { signal }),
|
|
138
|
+
signal,
|
|
139
|
+
);
|
|
140
|
+
if (signal.aborted) throw abortReason(signal);
|
|
141
|
+
if (!provided) throw new Error("provider returned no resource");
|
|
142
|
+
const image = await this.resolveSource(provided);
|
|
143
|
+
this.providerCounters.loaded += 1;
|
|
144
|
+
this.providerCounters.encodedBytes += providedByteLength(provided);
|
|
145
|
+
return { image, available: true };
|
|
146
|
+
} catch (error) {
|
|
147
|
+
if (signal.aborted || isAbortError(error)) {
|
|
148
|
+
this.providerCounters.cancellations += 1;
|
|
149
|
+
throw error;
|
|
150
|
+
}
|
|
151
|
+
this.providerCounters.failures += 1;
|
|
152
|
+
warnMissingResource(resource, error);
|
|
153
|
+
return {
|
|
154
|
+
image: normalizeSource(await this.decode(transparentPngBlob())),
|
|
155
|
+
available: false,
|
|
156
|
+
};
|
|
157
|
+
} finally {
|
|
158
|
+
this.providerCounters.resolveMs += Math.max(0, performance.now() - started);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private async resolveSource(provided: ProvidedResource): Promise<BrowserImageSource> {
|
|
163
|
+
const source = provided.source;
|
|
164
|
+
if (source instanceof Blob) return normalizeSource(await this.decode(source));
|
|
165
|
+
if (source instanceof ArrayBuffer) return normalizeSource(await this.decode(new Blob([source])));
|
|
166
|
+
if (ArrayBuffer.isView(source)) {
|
|
167
|
+
const view = new Uint8Array(source.buffer, source.byteOffset, source.byteLength);
|
|
168
|
+
const bytes = new Uint8Array(view.byteLength);
|
|
169
|
+
bytes.set(view);
|
|
170
|
+
return normalizeSource(await this.decode(new Blob([bytes])));
|
|
171
|
+
}
|
|
172
|
+
const dimensions = sourceDimensions(source);
|
|
173
|
+
if (!dimensions) throw new Error("direct image source has no finite dimensions");
|
|
174
|
+
return { source, ...dimensions };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function resourceIdentity(resource: Pick<ResourceDescriptor, "id">): string {
|
|
179
|
+
if (!resource.id) throw new Error("semantic resource stable id is required");
|
|
180
|
+
return resource.id;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
class BoundedScheduler {
|
|
184
|
+
private readonly limit: number;
|
|
185
|
+
private active = 0;
|
|
186
|
+
private peak = 0;
|
|
187
|
+
private readonly queue: Array<() => void> = [];
|
|
188
|
+
|
|
189
|
+
constructor(limit: number) {
|
|
190
|
+
if (!Number.isInteger(limit) || limit <= 0) throw new Error("semantic resource concurrency must be a positive integer");
|
|
191
|
+
this.limit = limit;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
run<T>(task: () => Promise<T>, signal: AbortSignal): Promise<T> {
|
|
195
|
+
return new Promise<T>((resolve, reject) => {
|
|
196
|
+
const start = () => {
|
|
197
|
+
if (signal.aborted) {
|
|
198
|
+
reject(abortReason(signal));
|
|
199
|
+
this.drain();
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
this.active += 1;
|
|
203
|
+
this.peak = Math.max(this.peak, this.active);
|
|
204
|
+
void task().then(resolve, reject).finally(() => {
|
|
205
|
+
this.active -= 1;
|
|
206
|
+
this.drain();
|
|
207
|
+
});
|
|
208
|
+
};
|
|
209
|
+
this.queue.push(start);
|
|
210
|
+
this.drain();
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
stats(): { active: number; queued: number; limit: number; peak: number } {
|
|
215
|
+
return { active: this.active, queued: this.queue.length, limit: this.limit, peak: this.peak };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
private drain(): void {
|
|
219
|
+
while (this.active < this.limit) {
|
|
220
|
+
const start = this.queue.shift();
|
|
221
|
+
if (!start) return;
|
|
222
|
+
start();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function sourceDimensions(source: TexImageSource): { width: number; height: number } | null {
|
|
228
|
+
const value = source as unknown as Record<string, unknown>;
|
|
229
|
+
const width = firstPositiveDimension(value.videoWidth, value.naturalWidth, value.displayWidth, value.width);
|
|
230
|
+
const height = firstPositiveDimension(value.videoHeight, value.naturalHeight, value.displayHeight, value.height);
|
|
231
|
+
return width != null && height != null
|
|
232
|
+
? { width, height }
|
|
233
|
+
: null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function decodedBytes(source: BrowserImageSource): number {
|
|
237
|
+
return Math.max(0, Math.floor(source.width * source.height * 4));
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function providedByteLength(provided: ProvidedResource): number {
|
|
241
|
+
const source = provided.source;
|
|
242
|
+
if (source instanceof Blob) return source.size;
|
|
243
|
+
if (source instanceof ArrayBuffer) return source.byteLength;
|
|
244
|
+
if (ArrayBuffer.isView(source)) return source.byteLength;
|
|
245
|
+
return 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function normalizeSource<T extends TexImageSource>(source: T): BrowserImageSource & { source: T } {
|
|
249
|
+
const dimensions = sourceDimensions(source);
|
|
250
|
+
if (!dimensions) throw new Error("decoded image source has no finite dimensions");
|
|
251
|
+
return { source, ...dimensions };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function firstPositiveDimension(...values: unknown[]): number | null {
|
|
255
|
+
for (const value of values) {
|
|
256
|
+
const dimension = numericDimension(value);
|
|
257
|
+
if (Number.isFinite(dimension) && dimension > 0) return dimension;
|
|
258
|
+
}
|
|
259
|
+
return null;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function numericDimension(value: unknown): number {
|
|
263
|
+
if (value && typeof value === "object") {
|
|
264
|
+
const animated = value as { baseVal?: { value?: unknown }; animVal?: { value?: unknown } };
|
|
265
|
+
const resolved = animated.animVal?.value ?? animated.baseVal?.value;
|
|
266
|
+
if (resolved != null) return Number(resolved);
|
|
267
|
+
}
|
|
268
|
+
return Number(value);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function closeImageSource(source: TexImageSource): void {
|
|
272
|
+
const close = (source as { close?: unknown }).close;
|
|
273
|
+
if (typeof close === "function") close.call(source);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function isAbortError(error: unknown): boolean {
|
|
277
|
+
return error instanceof DOMException && error.name === "AbortError";
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
function abortReason(signal: AbortSignal): unknown {
|
|
281
|
+
return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const TRANSPARENT_PNG_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR4nGNgYGBgAAAABQABpfZFQAAAAABJRU5ErkJggg==";
|
|
285
|
+
|
|
286
|
+
function transparentPngBlob(): Blob {
|
|
287
|
+
const bytes = Uint8Array.from(atob(TRANSPARENT_PNG_BASE64), (value) => value.charCodeAt(0));
|
|
288
|
+
return new Blob([bytes], { type: "image/png" });
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function warnMissingResource(resource: ResourceDescriptor, error: unknown): void {
|
|
292
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
293
|
+
console.warn(`[sekai-custom-profile-sdk] semantic resource unavailable ${resource.namespace}/${resource.key} role=${resource.role}: ${reason}; using transparent placeholder`);
|
|
294
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type { GlyphInstance } from "../types/glyph.js";
|
|
2
|
+
import type { SemanticDrawOperation } from "./semanticCommandPlanner.js";
|
|
3
|
+
|
|
4
|
+
const TEXT_SCALE = 2;
|
|
5
|
+
|
|
6
|
+
type RenderPlacement = {
|
|
7
|
+
anchor_x: number;
|
|
8
|
+
baseline: number | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Applies General's measured draw anchors after the Rust TMP layout has
|
|
13
|
+
* finished. The returned instances retain the exact layout metrics and glyph
|
|
14
|
+
* relationships; only final local/device draw coordinates are translated.
|
|
15
|
+
*/
|
|
16
|
+
export function placeGeneralTextInstances(
|
|
17
|
+
instances: readonly GlyphInstance[],
|
|
18
|
+
operations: readonly SemanticDrawOperation[],
|
|
19
|
+
): GlyphInstance[] {
|
|
20
|
+
const operationByCommand = new Map(
|
|
21
|
+
operations
|
|
22
|
+
.filter((operation) => operation.command.payload.kind === "text")
|
|
23
|
+
.map((operation) => [operation.command.id, operation] as const),
|
|
24
|
+
);
|
|
25
|
+
const translationByCommand = new Map<string, Translation>();
|
|
26
|
+
|
|
27
|
+
return instances.map((instance) => {
|
|
28
|
+
const operation = operationByCommand.get(instance.layerId);
|
|
29
|
+
if (!operation) return cloneInstance(instance);
|
|
30
|
+
const placement = renderPlacement(operation.command.render_placement);
|
|
31
|
+
if (!placement) return cloneInstance(instance);
|
|
32
|
+
let translation = translationByCommand.get(instance.layerId);
|
|
33
|
+
if (!translation) {
|
|
34
|
+
translation = placementTranslation(instance, operation, placement);
|
|
35
|
+
translationByCommand.set(instance.layerId, translation);
|
|
36
|
+
}
|
|
37
|
+
return translateInstance(instance, translation);
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type Translation = {
|
|
42
|
+
localX: number;
|
|
43
|
+
localY: number;
|
|
44
|
+
deviceX: number;
|
|
45
|
+
deviceY: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function placementTranslation(
|
|
49
|
+
instance: GlyphInstance,
|
|
50
|
+
operation: SemanticDrawOperation,
|
|
51
|
+
placement: RenderPlacement,
|
|
52
|
+
): Translation {
|
|
53
|
+
const alignment = numericAlignment(operation.command.payload.alignment);
|
|
54
|
+
const autoAnchor = alignment === 2
|
|
55
|
+
? 0
|
|
56
|
+
: alignment === 4
|
|
57
|
+
? instance.layoutMetrics.boxW / 2
|
|
58
|
+
: -instance.layoutMetrics.boxW / 2;
|
|
59
|
+
const localX = placement.anchor_x - autoAnchor;
|
|
60
|
+
const localY = placement.baseline == null
|
|
61
|
+
? 0
|
|
62
|
+
: placement.baseline - instance.layoutMetrics.anchorBase;
|
|
63
|
+
const commandMatrix = matrix(operation.command.matrix);
|
|
64
|
+
const combined = multiply(operation.baseMatrix, commandMatrix);
|
|
65
|
+
const scaledX = localX * TEXT_SCALE;
|
|
66
|
+
const scaledY = localY * TEXT_SCALE;
|
|
67
|
+
return {
|
|
68
|
+
localX,
|
|
69
|
+
localY,
|
|
70
|
+
deviceX: combined[0] * scaledX + combined[2] * scaledY,
|
|
71
|
+
deviceY: combined[1] * scaledX + combined[3] * scaledY,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function translateInstance(instance: GlyphInstance, value: Translation): GlyphInstance {
|
|
76
|
+
const localDeviceX = value.localX * TEXT_SCALE;
|
|
77
|
+
const localDeviceY = value.localY * TEXT_SCALE;
|
|
78
|
+
const output = cloneInstance(instance);
|
|
79
|
+
output.charOp[1] += value.localX;
|
|
80
|
+
output.charOp[2] += value.localY;
|
|
81
|
+
output.charPosition[1] += localDeviceX;
|
|
82
|
+
output.charPosition[2] -= localDeviceY;
|
|
83
|
+
output.charQuad[1] = shiftPoints(output.charQuad[1], localDeviceX, -localDeviceY);
|
|
84
|
+
output.quad = output.quad.map(([x, y, u, v, spread]) => [
|
|
85
|
+
x + value.deviceX,
|
|
86
|
+
y + value.deviceY,
|
|
87
|
+
u,
|
|
88
|
+
v,
|
|
89
|
+
spread,
|
|
90
|
+
]);
|
|
91
|
+
output.deviceCharPosition[1] += value.deviceX;
|
|
92
|
+
output.deviceCharPosition[2] += value.deviceY;
|
|
93
|
+
output.deviceCharQuad[1] = shiftPoints(
|
|
94
|
+
output.deviceCharQuad[1],
|
|
95
|
+
value.deviceX,
|
|
96
|
+
value.deviceY,
|
|
97
|
+
);
|
|
98
|
+
output.deviceGlyphQuad[1] = shiftPoints(
|
|
99
|
+
output.deviceGlyphQuad[1],
|
|
100
|
+
value.deviceX,
|
|
101
|
+
value.deviceY,
|
|
102
|
+
);
|
|
103
|
+
return output;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function cloneInstance(instance: GlyphInstance): GlyphInstance {
|
|
107
|
+
return {
|
|
108
|
+
...instance,
|
|
109
|
+
quad: instance.quad.map((value) => [...value]),
|
|
110
|
+
charPosition: [...instance.charPosition],
|
|
111
|
+
charOp: [...instance.charOp],
|
|
112
|
+
charQuad: [instance.charQuad[0], instance.charQuad[1].map((value) => [...value])],
|
|
113
|
+
deviceCharPosition: [...instance.deviceCharPosition],
|
|
114
|
+
deviceCharQuad: [
|
|
115
|
+
instance.deviceCharQuad[0],
|
|
116
|
+
instance.deviceCharQuad[1].map((value) => [...value]),
|
|
117
|
+
],
|
|
118
|
+
deviceGlyphQuad: [
|
|
119
|
+
instance.deviceGlyphQuad[0],
|
|
120
|
+
instance.deviceGlyphQuad[1].map((value) => [...value]),
|
|
121
|
+
],
|
|
122
|
+
layoutMetrics: {
|
|
123
|
+
...instance.layoutMetrics,
|
|
124
|
+
lineWidths: [...instance.layoutMetrics.lineWidths],
|
|
125
|
+
rectWidths: [...instance.layoutMetrics.rectWidths],
|
|
126
|
+
lineOffsets: [...instance.layoutMetrics.lineOffsets],
|
|
127
|
+
},
|
|
128
|
+
fill: [...instance.fill],
|
|
129
|
+
outline: [...instance.outline],
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function shiftPoints(
|
|
134
|
+
points: Array<[number, number]>,
|
|
135
|
+
dx: number,
|
|
136
|
+
dy: number,
|
|
137
|
+
): Array<[number, number]> {
|
|
138
|
+
return points.map(([x, y]) => [x + dx, y + dy]);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function renderPlacement(value: unknown): RenderPlacement | null {
|
|
142
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
143
|
+
const record = value as Record<string, unknown>;
|
|
144
|
+
if (!Number.isFinite(record.anchor_x)) return null;
|
|
145
|
+
if (record.baseline != null && !Number.isFinite(record.baseline)) return null;
|
|
146
|
+
return {
|
|
147
|
+
anchor_x: Number(record.anchor_x),
|
|
148
|
+
baseline: record.baseline == null ? null : Number(record.baseline),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function numericAlignment(value: unknown): number {
|
|
153
|
+
return Number.isInteger(value) ? Number(value) & 0x07 : 1;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function matrix(value: unknown): [number, number, number, number, number, number] {
|
|
157
|
+
if (!Array.isArray(value) || value.length !== 6 || value.some((part) => !Number.isFinite(part))) {
|
|
158
|
+
return [1, 0, 0, 1, 0, 0];
|
|
159
|
+
}
|
|
160
|
+
return value.map(Number) as [number, number, number, number, number, number];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function multiply(
|
|
164
|
+
parent: [number, number, number, number, number, number],
|
|
165
|
+
child: [number, number, number, number, number, number],
|
|
166
|
+
): [number, number, number, number, number, number] {
|
|
167
|
+
return [
|
|
168
|
+
parent[0] * child[0] + parent[2] * child[1],
|
|
169
|
+
parent[1] * child[0] + parent[3] * child[1],
|
|
170
|
+
parent[0] * child[2] + parent[2] * child[3],
|
|
171
|
+
parent[1] * child[2] + parent[3] * child[3],
|
|
172
|
+
parent[0] * child[4] + parent[2] * child[5] + parent[4],
|
|
173
|
+
parent[1] * child[4] + parent[3] * child[5] + parent[5],
|
|
174
|
+
];
|
|
175
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export function packPreviewTransformsForTexture(transforms: Float32Array, width: number): Float32Array {
|
|
2
|
+
if (!Number.isInteger(width) || width < 1 || transforms.length !== width * 8) {
|
|
3
|
+
throw new Error(`invalid preview transform buffer ${transforms.length} for width ${width}`);
|
|
4
|
+
}
|
|
5
|
+
const packed = new Float32Array(transforms.length);
|
|
6
|
+
for (let slot = 0; slot < width; slot += 1) {
|
|
7
|
+
const source = slot * 8;
|
|
8
|
+
packed.set(transforms.subarray(source, source + 4), slot * 4);
|
|
9
|
+
packed.set(transforms.subarray(source + 4, source + 8), (width + slot) * 4);
|
|
10
|
+
}
|
|
11
|
+
return packed;
|
|
12
|
+
}
|