@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.
Files changed (130) hide show
  1. package/LICENSE +235 -0
  2. package/LICENSE-EXCEPTION +32 -0
  3. package/NOTICE +11 -0
  4. package/README.en.md +456 -0
  5. package/README.md +458 -0
  6. package/dist/allium_renderer_wasm.js +14 -0
  7. package/dist/allium_renderer_wasm.wasm +0 -0
  8. package/dist/authoring.d.ts +14 -0
  9. package/dist/authoring.js +34 -0
  10. package/dist/cache/glyphPersistentCache.d.ts +145 -0
  11. package/dist/cache/glyphPersistentCache.js +346 -0
  12. package/dist/cache/indexedDbGlyphRecordStore.d.ts +26 -0
  13. package/dist/cache/indexedDbGlyphRecordStore.js +214 -0
  14. package/dist/cache/sessionImageResourceCache.d.ts +39 -0
  15. package/dist/cache/sessionImageResourceCache.js +156 -0
  16. package/dist/emscripten.d.ts +11 -0
  17. package/dist/emscripten.js +1 -0
  18. package/dist/fontProvider.d.ts +36 -0
  19. package/dist/fontProvider.js +84 -0
  20. package/dist/fontSdfAtlas.d.ts +92 -0
  21. package/dist/fontSdfAtlas.js +296 -0
  22. package/dist/gpu/browserSemanticResources.d.ts +64 -0
  23. package/dist/gpu/browserSemanticResources.js +232 -0
  24. package/dist/gpu/generalTextRenderPlacement.d.ts +8 -0
  25. package/dist/gpu/generalTextRenderPlacement.js +131 -0
  26. package/dist/gpu/previewTransformTextureLayout.d.ts +1 -0
  27. package/dist/gpu/previewTransformTextureLayout.js +12 -0
  28. package/dist/gpu/semanticCommandGeometry.d.ts +21 -0
  29. package/dist/gpu/semanticCommandGeometry.js +226 -0
  30. package/dist/gpu/semanticCommandPlanner.d.ts +131 -0
  31. package/dist/gpu/semanticCommandPlanner.js +203 -0
  32. package/dist/gpu/semanticTextGlyphBridge.d.ts +3 -0
  33. package/dist/gpu/semanticTextGlyphBridge.js +22 -0
  34. package/dist/gpu/semanticWebglSceneRenderer.d.ts +70 -0
  35. package/dist/gpu/semanticWebglSceneRenderer.js +211 -0
  36. package/dist/gpu/slotRanges.d.ts +5 -0
  37. package/dist/gpu/slotRanges.js +14 -0
  38. package/dist/gpu/webglSdfAtlasTexture.d.ts +17 -0
  39. package/dist/gpu/webglSdfAtlasTexture.js +72 -0
  40. package/dist/gpu/webglSdfGlyphPipeline.d.ts +26 -0
  41. package/dist/gpu/webglSdfGlyphPipeline.js +267 -0
  42. package/dist/gpu/webglSemanticCommandExecutor.d.ts +79 -0
  43. package/dist/gpu/webglSemanticCommandExecutor.js +686 -0
  44. package/dist/index.d.ts +18 -0
  45. package/dist/index.js +8 -0
  46. package/dist/interaction/numericTextRegions.d.ts +46 -0
  47. package/dist/interaction/numericTextRegions.js +44 -0
  48. package/dist/localizationProvider.d.ts +33 -0
  49. package/dist/localizationProvider.js +74 -0
  50. package/dist/originPrebuiltSdfAtlasPackage.d.ts +39 -0
  51. package/dist/originPrebuiltSdfAtlasPackage.js +316 -0
  52. package/dist/prebuiltSdfAtlas.d.ts +36 -0
  53. package/dist/prebuiltSdfAtlas.js +261 -0
  54. package/dist/protocol.d.ts +456 -0
  55. package/dist/protocol.js +1 -0
  56. package/dist/renderer.d.ts +218 -0
  57. package/dist/renderer.js +641 -0
  58. package/dist/resourceProvider.d.ts +24 -0
  59. package/dist/resourceProvider.js +41 -0
  60. package/dist/telemetry/rendererTelemetry.d.ts +158 -0
  61. package/dist/telemetry/rendererTelemetry.js +232 -0
  62. package/dist/third-party/freetype/FTL.txt +169 -0
  63. package/dist/types/atlas.d.ts +90 -0
  64. package/dist/types/atlas.js +1 -0
  65. package/dist/types/authoring.d.ts +81 -0
  66. package/dist/types/authoring.js +1 -0
  67. package/dist/types/core.d.ts +128 -0
  68. package/dist/types/core.js +1 -0
  69. package/dist/types/freeType.d.ts +54 -0
  70. package/dist/types/freeType.js +13 -0
  71. package/dist/types/glyph.d.ts +33 -0
  72. package/dist/types/glyph.js +1 -0
  73. package/dist/types/layout.d.ts +25 -0
  74. package/dist/types/layout.js +1 -0
  75. package/dist/types/text.d.ts +28 -0
  76. package/dist/types/text.js +1 -0
  77. package/dist/worker/glyphWorkScheduler.d.ts +29 -0
  78. package/dist/worker/glyphWorkScheduler.js +107 -0
  79. package/dist/worker-client.d.ts +114 -0
  80. package/dist/worker-client.js +413 -0
  81. package/dist/worker.d.ts +1 -0
  82. package/dist/worker.js +708 -0
  83. package/package.json +46 -0
  84. package/src/atlas.rs +652 -0
  85. package/src/authoring.ts +42 -0
  86. package/src/authoring_runtime.rs +303 -0
  87. package/src/cache/glyphPersistentCache.ts +463 -0
  88. package/src/cache/indexedDbGlyphRecordStore.ts +238 -0
  89. package/src/cache/sessionImageResourceCache.ts +186 -0
  90. package/src/edt.rs +97 -0
  91. package/src/emscripten.ts +17 -0
  92. package/src/fontProvider.ts +127 -0
  93. package/src/fontSdfAtlas.ts +431 -0
  94. package/src/geometry.rs +149 -0
  95. package/src/glyph_plan.rs +226 -0
  96. package/src/gpu/browserSemanticResources.ts +294 -0
  97. package/src/gpu/generalTextRenderPlacement.ts +175 -0
  98. package/src/gpu/previewTransformTextureLayout.ts +12 -0
  99. package/src/gpu/semanticCommandGeometry.ts +260 -0
  100. package/src/gpu/semanticCommandPlanner.ts +280 -0
  101. package/src/gpu/semanticTextGlyphBridge.ts +35 -0
  102. package/src/gpu/semanticWebglSceneRenderer.ts +261 -0
  103. package/src/gpu/slotRanges.ts +15 -0
  104. package/src/gpu/webglSdfAtlasTexture.ts +69 -0
  105. package/src/gpu/webglSdfGlyphPipeline.ts +299 -0
  106. package/src/gpu/webglSemanticCommandExecutor.ts +739 -0
  107. package/src/index.ts +84 -0
  108. package/src/interaction/numericTextRegions.ts +80 -0
  109. package/src/layout.rs +1882 -0
  110. package/src/lib.rs +1443 -0
  111. package/src/localizationProvider.ts +113 -0
  112. package/src/main.rs +55 -0
  113. package/src/masterdata_runtime.rs +496 -0
  114. package/src/originPrebuiltSdfAtlasPackage.ts +403 -0
  115. package/src/prebuiltSdfAtlas.ts +306 -0
  116. package/src/protocol.ts +150 -0
  117. package/src/renderer.ts +792 -0
  118. package/src/resourceProvider.ts +69 -0
  119. package/src/scene.rs +665 -0
  120. package/src/telemetry/rendererTelemetry.ts +358 -0
  121. package/src/types/atlas.ts +80 -0
  122. package/src/types/authoring.ts +85 -0
  123. package/src/types/core.ts +110 -0
  124. package/src/types/freeType.ts +63 -0
  125. package/src/types/glyph.ts +33 -0
  126. package/src/types/layout.ts +28 -0
  127. package/src/types/text.ts +28 -0
  128. package/src/worker/glyphWorkScheduler.ts +130 -0
  129. package/src/worker-client.ts +484 -0
  130. package/src/worker.ts +895 -0
@@ -0,0 +1,186 @@
1
+ export type DecodedImageValue<T> = { value: T; bytes: number };
2
+
3
+ export type ImageResourceLease<T> = {
4
+ value: T;
5
+ release(): void;
6
+ };
7
+
8
+ type CacheEntry<T> = {
9
+ value: T;
10
+ bytes: number;
11
+ refs: number;
12
+ touched: number;
13
+ };
14
+
15
+ type InflightEntry<T> = {
16
+ promise: Promise<CacheEntry<T>>;
17
+ controller: AbortController;
18
+ waiters: number;
19
+ settled: boolean;
20
+ };
21
+
22
+ export class SessionImageResourceCache<T> {
23
+ private readonly budget: { softBytes: number; hardBytes: number; dispose?: (value: T) => void };
24
+ private readonly entries = new Map<string, CacheEntry<T>>();
25
+ private readonly inflight = new Map<string, InflightEntry<T>>();
26
+ private clock = 0;
27
+ private totalBytes = 0;
28
+ private loads = 0;
29
+ private hits = 0;
30
+ private evictions = 0;
31
+
32
+ constructor(budget: { softBytes: number; hardBytes: number; dispose?: (value: T) => void }) {
33
+ if (!Number.isFinite(budget.softBytes) || !Number.isFinite(budget.hardBytes)
34
+ || budget.softBytes < 0 || budget.hardBytes <= 0 || budget.softBytes > budget.hardBytes) {
35
+ throw new Error("invalid image cache byte budget");
36
+ }
37
+ this.budget = budget;
38
+ }
39
+
40
+ async acquire(
41
+ key: string,
42
+ loader: (signal: AbortSignal) => Promise<DecodedImageValue<T>>,
43
+ signal: AbortSignal = new AbortController().signal,
44
+ ): Promise<ImageResourceLease<T>> {
45
+ if (signal.aborted) throw abortReason(signal);
46
+ const cached = this.entries.get(key);
47
+ if (cached) {
48
+ this.hits += 1;
49
+ cached.refs += 1;
50
+ cached.touched = ++this.clock;
51
+ return this.lease(cached);
52
+ }
53
+ let pending = this.inflight.get(key);
54
+ if (pending) {
55
+ this.hits += 1;
56
+ } else {
57
+ this.loads += 1;
58
+ const controller = new AbortController();
59
+ const created: InflightEntry<T> = {
60
+ controller,
61
+ waiters: 0,
62
+ settled: false,
63
+ promise: undefined as unknown as Promise<CacheEntry<T>>,
64
+ };
65
+ created.promise = Promise.resolve().then(() => this.load(key, () => loader(controller.signal), created));
66
+ pending = created;
67
+ this.inflight.set(key, pending);
68
+ void pending.promise.finally(() => {
69
+ pending!.settled = true;
70
+ if (this.inflight.get(key) === pending) this.inflight.delete(key);
71
+ }).catch(() => undefined);
72
+ }
73
+ pending.waiters += 1;
74
+ try {
75
+ const entry = await awaitWithAbort(pending.promise, signal);
76
+ entry.refs += 1;
77
+ entry.touched = ++this.clock;
78
+ return this.lease(entry);
79
+ } finally {
80
+ pending.waiters = Math.max(0, pending.waiters - 1);
81
+ if (pending.waiters === 0 && !pending.settled) {
82
+ if (this.inflight.get(key) === pending) this.inflight.delete(key);
83
+ pending.controller.abort(new DOMException("No resource waiters remain", "AbortError"));
84
+ }
85
+ this.trimSoft();
86
+ }
87
+ }
88
+
89
+ stats(): { entries: number; bytes: number; pinned: number; loads: number; hits: number; evictions: number } {
90
+ return {
91
+ entries: this.entries.size,
92
+ bytes: this.totalBytes,
93
+ pinned: [...this.entries.values()].filter((entry) => entry.refs > 0).length,
94
+ loads: this.loads,
95
+ hits: this.hits,
96
+ evictions: this.evictions,
97
+ };
98
+ }
99
+
100
+ keys(): string[] {
101
+ return [...this.entries.entries()]
102
+ .sort(([, left], [, right]) => left.touched - right.touched)
103
+ .map(([key]) => key);
104
+ }
105
+
106
+ clear(): void {
107
+ for (const [key, entry] of this.entries) {
108
+ if (entry.refs === 0) this.remove(key, entry);
109
+ }
110
+ }
111
+
112
+ private async load(
113
+ key: string,
114
+ loader: () => Promise<DecodedImageValue<T>>,
115
+ owner: InflightEntry<T>,
116
+ ): Promise<CacheEntry<T>> {
117
+ const decoded = await loader();
118
+ if (this.inflight.get(key) !== owner) {
119
+ this.budget.dispose?.(decoded.value);
120
+ throw abortReason(owner.controller.signal);
121
+ }
122
+ if (!Number.isFinite(decoded.bytes) || !Number.isInteger(decoded.bytes) || decoded.bytes < 0) {
123
+ throw new Error(`invalid decoded image byte size ${decoded.bytes}`);
124
+ }
125
+ if (decoded.bytes > this.budget.hardBytes) {
126
+ throw new Error(`decoded image exceeds hard byte budget: ${decoded.bytes} > ${this.budget.hardBytes}`);
127
+ }
128
+ this.evictUntil(this.budget.hardBytes - decoded.bytes);
129
+ if (this.totalBytes + decoded.bytes > this.budget.hardBytes) {
130
+ throw new Error(`decoded image cache hard byte budget is pinned`);
131
+ }
132
+ const entry: CacheEntry<T> = { value: decoded.value, bytes: decoded.bytes, refs: 0, touched: ++this.clock };
133
+ this.entries.set(key, entry);
134
+ this.totalBytes += decoded.bytes;
135
+ return entry;
136
+ }
137
+
138
+ private lease(entry: CacheEntry<T>): ImageResourceLease<T> {
139
+ let released = false;
140
+ return {
141
+ value: entry.value,
142
+ release: () => {
143
+ if (released) return;
144
+ released = true;
145
+ entry.refs = Math.max(0, entry.refs - 1);
146
+ entry.touched = ++this.clock;
147
+ this.trimSoft();
148
+ },
149
+ };
150
+ }
151
+
152
+ private trimSoft(): void {
153
+ this.evictUntil(this.budget.softBytes);
154
+ }
155
+
156
+ private evictUntil(targetBytes: number): void {
157
+ if (this.totalBytes <= targetBytes) return;
158
+ const candidates = [...this.entries.entries()]
159
+ .filter(([, entry]) => entry.refs === 0)
160
+ .sort(([, left], [, right]) => left.touched - right.touched);
161
+ for (const [key, entry] of candidates) {
162
+ if (this.totalBytes <= targetBytes) break;
163
+ this.remove(key, entry);
164
+ }
165
+ }
166
+
167
+ private remove(key: string, entry: CacheEntry<T>): void {
168
+ if (!this.entries.delete(key)) return;
169
+ this.totalBytes -= entry.bytes;
170
+ this.evictions += 1;
171
+ this.budget.dispose?.(entry.value);
172
+ }
173
+ }
174
+
175
+ function awaitWithAbort<T>(promise: Promise<T>, signal: AbortSignal): Promise<T> {
176
+ if (signal.aborted) return Promise.reject(abortReason(signal));
177
+ return new Promise<T>((resolve, reject) => {
178
+ const abort = () => reject(abortReason(signal));
179
+ signal.addEventListener("abort", abort, { once: true });
180
+ void promise.then(resolve, reject).finally(() => signal.removeEventListener("abort", abort));
181
+ });
182
+ }
183
+
184
+ function abortReason(signal: AbortSignal): unknown {
185
+ return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
186
+ }
package/src/edt.rs ADDED
@@ -0,0 +1,97 @@
1
+ //! Two-dimensional Euclidean distance transform (Felzenszwalb and Huttenlocher, 2012).
2
+ //!
3
+ //! Exact O(n) squared Euclidean distance transform, evaluated by columns and rows.
4
+
5
+ const INF: f32 = 1e20;
6
+
7
+ fn dt_1d(f: &[f32], d: &mut [f32], v: &mut [usize], z: &mut [f32]) {
8
+ let n = f.len();
9
+ if n == 0 {
10
+ return;
11
+ }
12
+ let mut k: usize = 0;
13
+ v[0] = 0;
14
+ z[0] = -INF;
15
+ z[1] = INF;
16
+ for q in 1..n {
17
+ let mut s = intersection(f, q, v[k]);
18
+ while s <= z[k] {
19
+ k -= 1;
20
+ s = intersection(f, q, v[k]);
21
+ }
22
+ k += 1;
23
+ v[k] = q;
24
+ z[k] = s;
25
+ z[k + 1] = INF;
26
+ }
27
+
28
+ let mut k = 0usize;
29
+ for q in 0..n {
30
+ while z[k + 1] < q as f32 {
31
+ k += 1;
32
+ }
33
+ let dq = q as f32 - v[k] as f32;
34
+ d[q] = dq * dq + f[v[k]];
35
+ }
36
+ }
37
+ #[inline]
38
+ fn intersection(f: &[f32], q: usize, vk: usize) -> f32 {
39
+ let fq = f[q];
40
+ let fv = f[vk];
41
+ ((fq + (q * q) as f32) - (fv + (vk * vk) as f32)) / (2.0 * q as f32 - 2.0 * vk as f32)
42
+ }
43
+
44
+ pub fn edt_2d_sq(grid: &mut [f32], width: usize, height: usize) {
45
+ if width == 0 || height == 0 {
46
+ return;
47
+ }
48
+ let cap = width.max(height);
49
+ let mut f = vec![0.0f32; cap];
50
+ let mut d = vec![0.0f32; cap];
51
+ let mut v = vec![0usize; cap + 1];
52
+ let mut z = vec![0.0f32; cap + 1];
53
+
54
+ for x in 0..width {
55
+ for y in 0..height {
56
+ f[y] = grid[y * width + x];
57
+ }
58
+ dt_1d(&f[..height], &mut d[..height], &mut v, &mut z);
59
+ for y in 0..height {
60
+ grid[y * width + x] = d[y];
61
+ }
62
+ }
63
+
64
+ for y in 0..height {
65
+ let row = &mut grid[y * width..y * width + width];
66
+ f[..width].copy_from_slice(row);
67
+ dt_1d(&f[..width], &mut d[..width], &mut v, &mut z);
68
+ row.copy_from_slice(&d[..width]);
69
+ }
70
+ }
71
+
72
+ pub fn signed_distance_from_mask(inside: &[bool], width: usize, height: usize) -> Vec<f32> {
73
+ let n = width * height;
74
+ let mut outside_field = vec![0.0f32; n];
75
+ let mut inside_field = vec![0.0f32; n];
76
+ for i in 0..n {
77
+ if inside[i] {
78
+ outside_field[i] = INF;
79
+ } else {
80
+ inside_field[i] = INF;
81
+ }
82
+ }
83
+ edt_2d_sq(&mut outside_field, width, height);
84
+ edt_2d_sq(&mut inside_field, width, height);
85
+
86
+ let mut out = vec![0.0f32; n];
87
+ let cap = ((width * width + height * height) as f32).sqrt();
88
+ for i in 0..n {
89
+ let dist = if inside[i] {
90
+ -outside_field[i].max(0.0).sqrt().min(cap)
91
+ } else {
92
+ inside_field[i].max(0.0).sqrt().min(cap)
93
+ };
94
+ out[i] = dist;
95
+ }
96
+ out
97
+ }
@@ -0,0 +1,17 @@
1
+ export type EmscriptenModule = {
2
+ HEAPU8: Uint8Array;
3
+ HEAPU32: Uint32Array;
4
+ _malloc(size: number): number;
5
+ _free(pointer: number): void;
6
+ ccall(
7
+ name: string,
8
+ returnType: "number" | null,
9
+ argumentTypes: Array<"number" | "string">,
10
+ arguments_: Array<number | string>,
11
+ ): number | null;
12
+ };
13
+
14
+ export type EmscriptenModuleFactory = (options?: {
15
+ locateFile?: (path: string) => string;
16
+ printErr?: (...values: unknown[]) => void;
17
+ }) => Promise<EmscriptenModule>;
@@ -0,0 +1,127 @@
1
+ export type FontRequest = {
2
+ region: string;
3
+ family: string;
4
+ };
5
+
6
+ export type ProvidedFont = {
7
+ bytes: ArrayBuffer | Uint8Array;
8
+ };
9
+
10
+ export type FontContext = {
11
+ signal: AbortSignal;
12
+ };
13
+
14
+ /** Resolves a demanded font family through arbitrary caller-owned logic. */
15
+ export interface FontProvider {
16
+ provide(
17
+ request: FontRequest,
18
+ context: FontContext,
19
+ ): Promise<ProvidedFont | null>;
20
+ }
21
+
22
+ export type FontProviderManagerOptions = {
23
+ provider: FontProvider;
24
+ concurrency?: number;
25
+ };
26
+
27
+ export type FontProviderStats = {
28
+ requested: number;
29
+ unique: number;
30
+ loaded: number;
31
+ bytes: number;
32
+ failures: number;
33
+ active: number;
34
+ peakActive: number;
35
+ resolveMs: number;
36
+ };
37
+
38
+ export class FontProviderManager {
39
+ private readonly provider: FontProvider;
40
+ private readonly concurrency: number;
41
+ private readonly counters: FontProviderStats = {
42
+ requested: 0,
43
+ unique: 0,
44
+ loaded: 0,
45
+ bytes: 0,
46
+ failures: 0,
47
+ active: 0,
48
+ peakActive: 0,
49
+ resolveMs: 0,
50
+ };
51
+
52
+ constructor(options: FontProviderManagerOptions) {
53
+ if (!options.provider || typeof options.provider.provide !== "function") {
54
+ throw new Error("font provider is required");
55
+ }
56
+ const concurrency = options.concurrency ?? 4;
57
+ if (!Number.isInteger(concurrency) || concurrency <= 0) {
58
+ throw new Error("font concurrency must be a positive integer");
59
+ }
60
+ this.provider = options.provider;
61
+ this.concurrency = concurrency;
62
+ }
63
+
64
+ async resolve(
65
+ requests: readonly FontRequest[],
66
+ signal: AbortSignal = new AbortController().signal,
67
+ ): Promise<Map<string, ArrayBuffer>> {
68
+ const started = performance.now();
69
+ const unique = [...new Map(requests.map((request) => [
70
+ `${request.region}\0${request.family}`,
71
+ request,
72
+ ] as const)).values()];
73
+ this.counters.requested += requests.length;
74
+ this.counters.unique += unique.length;
75
+ const values = new Map<string, ArrayBuffer>();
76
+ let cursor = 0;
77
+ const worker = async () => {
78
+ while (cursor < unique.length) {
79
+ if (signal.aborted) throw abortReason(signal);
80
+ const request = unique[cursor++];
81
+ this.counters.active += 1;
82
+ this.counters.peakActive = Math.max(this.counters.peakActive, this.counters.active);
83
+ let provided: ProvidedFont | null;
84
+ try {
85
+ provided = await this.provider.provide(request, { signal });
86
+ } catch (error) {
87
+ this.counters.failures += 1;
88
+ throw error;
89
+ } finally {
90
+ this.counters.active -= 1;
91
+ }
92
+ const bytes = provided?.bytes;
93
+ if (!(bytes instanceof ArrayBuffer) && !(bytes instanceof Uint8Array)) {
94
+ this.counters.failures += 1;
95
+ throw new Error(`font provider returned no bytes for ${request.region}:${request.family}`);
96
+ }
97
+ const snapshot = bytes instanceof ArrayBuffer
98
+ ? bytes.slice(0)
99
+ : Uint8Array.from(bytes).buffer;
100
+ if (snapshot.byteLength === 0) {
101
+ this.counters.failures += 1;
102
+ throw new Error(`font provider returned an empty font for ${request.region}:${request.family}`);
103
+ }
104
+ this.counters.loaded += 1;
105
+ this.counters.bytes += snapshot.byteLength;
106
+ values.set(request.family, snapshot);
107
+ }
108
+ };
109
+ try {
110
+ await Promise.all(Array.from(
111
+ { length: Math.min(this.concurrency, unique.length) },
112
+ worker,
113
+ ));
114
+ return values;
115
+ } finally {
116
+ this.counters.resolveMs += performance.now() - started;
117
+ }
118
+ }
119
+
120
+ stats(): FontProviderStats {
121
+ return { ...this.counters };
122
+ }
123
+ }
124
+
125
+ function abortReason(signal: AbortSignal): unknown {
126
+ return signal.reason ?? new DOMException("The operation was aborted", "AbortError");
127
+ }