@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,260 @@
1
+ import type { SemanticDrawOperation, SemanticResourceKey } from "./semanticCommandPlanner.js";
2
+
3
+ export type SemanticDrawBatchKind = "shape" | "image" | "mask" | "text" | "composite";
4
+ export type SemanticBlendMode = "src_over" | "src_in" | "dst_in" | "multiply" | "screen" | "add";
5
+ export type SemanticCompositeOperation = "marker" | "begin_isolation" | "end_isolation";
6
+
7
+ export type SemanticDrawBatch = {
8
+ kind: SemanticDrawBatchKind;
9
+ commandIds: string[];
10
+ operations: SemanticDrawOperation[];
11
+ vertices: Float32Array;
12
+ layerSlots: Uint32Array;
13
+ commandSlots: Uint32Array;
14
+ resource: SemanticResourceKey | null;
15
+ maskResource: SemanticResourceKey | null;
16
+ blendMode: SemanticBlendMode;
17
+ compositeOperation: SemanticCompositeOperation | null;
18
+ };
19
+
20
+ export const SEMANTIC_FLOATS_PER_VERTEX = 28;
21
+ export function semanticTextBatchKey(commandIds: readonly string[]): string {
22
+ return `semantic-text-batch\0${commandIds.join("\0")}`;
23
+ }
24
+ const UNIT_TRIANGLES = [
25
+ [0, 0], [1, 0], [1, 1],
26
+ [0, 0], [1, 1], [0, 1],
27
+ ] as const;
28
+
29
+ /** Compile immutable geometry only. Render mask and dynamic translation remain
30
+ * in the dense layer-state textures, so toggles/ticks never rebuild vertices. */
31
+ export function compileSemanticDrawBatches(operations: SemanticDrawOperation[]): SemanticDrawBatch[] {
32
+ const groups: Array<{ key: string; kind: SemanticDrawBatchKind; resource: SemanticResourceKey | null; maskResource: SemanticResourceKey | null; blendMode: SemanticBlendMode; compositeOperation: SemanticCompositeOperation | null; operations: SemanticDrawOperation[] }> = [];
33
+ for (const operation of operations) {
34
+ const descriptor = batchDescriptor(operation);
35
+ const previous = groups.at(-1);
36
+ if (previous?.key === descriptor.key) previous.operations.push(operation);
37
+ else groups.push({ ...descriptor, operations: [operation] });
38
+ }
39
+ return groups.map((group) => compileGroup(group.kind, group.resource, group.maskResource, group.blendMode, group.compositeOperation, group.operations));
40
+ }
41
+
42
+ function batchDescriptor(operation: SemanticDrawOperation): {
43
+ key: string;
44
+ kind: SemanticDrawBatchKind;
45
+ resource: SemanticResourceKey | null;
46
+ maskResource: SemanticResourceKey | null;
47
+ blendMode: SemanticBlendMode;
48
+ compositeOperation: SemanticCompositeOperation | null;
49
+ } {
50
+ const payload = operation.command.payload;
51
+ const blendMode = commandBlendMode(operation.command.blend_mode);
52
+ if (payload.kind === "image") {
53
+ const resource = requireResource(payload.resource, operation.command.id);
54
+ const maskResource = optionalResource(payload.alpha_mask);
55
+ const maskKey = maskResource ? `\0${maskResource.namespace}\0${maskResource.key}` : "";
56
+ return { key: `image\0${blendMode}\0${resource.namespace}\0${resource.key}${maskKey}`, kind: "image", resource, maskResource, blendMode, compositeOperation: null };
57
+ }
58
+ if (payload.kind === "shape") {
59
+ const maskResource = assetMaskResource(payload.primitive);
60
+ if (maskResource) return { key: `mask\0${blendMode}\0${maskResource.namespace}\0${maskResource.key}`, kind: "mask", resource: maskResource, maskResource: null, blendMode, compositeOperation: null };
61
+ return { key: `shape\0${blendMode}`, kind: "shape", resource: null, maskResource: null, blendMode, compositeOperation: null };
62
+ }
63
+ if (payload.kind === "text") return { key: `text\0${blendMode}`, kind: "text", resource: null, maskResource: null, blendMode, compositeOperation: null };
64
+ if (payload.kind === "composite") {
65
+ const compositeOperation = requireCompositeOperation(payload.operation, operation.command.id);
66
+ return { key: `composite\0${operation.command.id}`, kind: "composite", resource: null, maskResource: null, blendMode, compositeOperation };
67
+ }
68
+ throw new Error(`unsupported semantic command payload ${(payload as { kind?: unknown }).kind}`);
69
+ }
70
+
71
+ function compileGroup(
72
+ kind: SemanticDrawBatchKind,
73
+ resource: SemanticResourceKey | null,
74
+ maskResource: SemanticResourceKey | null,
75
+ blendMode: SemanticBlendMode,
76
+ compositeOperation: SemanticCompositeOperation | null,
77
+ operations: SemanticDrawOperation[]
78
+ ): SemanticDrawBatch {
79
+ if (kind === "text" || kind === "composite") {
80
+ return { kind, resource, maskResource, blendMode, compositeOperation, operations: [...operations], commandIds: operations.map((op) => op.command.id), vertices: new Float32Array(), layerSlots: new Uint32Array(), commandSlots: new Uint32Array() };
81
+ }
82
+ const vertices = new Float32Array(operations.length * 6 * SEMANTIC_FLOATS_PER_VERTEX);
83
+ const layerSlots = new Uint32Array(operations.length * 6);
84
+ const commandSlots = new Uint32Array(operations.length * 6);
85
+ let vertexOffset = 0;
86
+ for (const operation of operations) {
87
+ const bounds = requireRect(operation.command.bounds, operation.command.id);
88
+ const commandMatrix = requireMatrix(operation.command.matrix, operation.command.id);
89
+ const clip = commandClip(operation.command.clip, operation.baseMatrix, operation.command.id);
90
+ const payload = operation.command.payload;
91
+ const uv = payload.kind === "image" ? optionalRect(payload.uv) : { x: 0, y: 0, width: 1, height: 1 };
92
+ const fill = payload.kind === "image" ? optionalColor(payload.tint, [1, 1, 1, 1]) : optionalColor(payload.fill, [1, 1, 1, 1]);
93
+ const stroke = payload.kind === "shape" ? optionalColor(payload.stroke, [0, 0, 0, 0]) : [0, 0, 0, 0];
94
+ const [primitive, radiusX, radiusY] = payload.kind === "shape"
95
+ ? shapeParams(payload.primitive, bounds)
96
+ : payload.kind === "image" ? imageClipParams(payload.clip, bounds) : [0, 0, 0];
97
+ const strokeWidth = payload.kind === "shape" && typeof payload.stroke_width === "number"
98
+ ? payload.stroke_width
99
+ : 0;
100
+ for (const [unitX, unitY] of UNIT_TRIANGLES) {
101
+ const vertexFill = payload.kind === "shape" ? gradientColor(payload.gradient, unitX, unitY, fill) : fill;
102
+ const localX = bounds.x + bounds.width * unitX;
103
+ const localY = bounds.y + bounds.height * unitY;
104
+ const commandPoint = transformPoint(commandMatrix, localX, localY);
105
+ const [x, y] = transformPoint(operation.baseMatrix, commandPoint[0], commandPoint[1]);
106
+ const base = vertexOffset * SEMANTIC_FLOATS_PER_VERTEX;
107
+ vertices.set([
108
+ x, y,
109
+ uv.x + uv.width * unitX, uv.y + uv.height * unitY,
110
+ unitX, unitY,
111
+ ...vertexFill,
112
+ ...stroke,
113
+ primitive, radiusX, radiusY, strokeWidth,
114
+ ...clip[0], ...clip[1], ...clip[2], ...clip[3],
115
+ bounds.width, bounds.height,
116
+ ], base);
117
+ layerSlots[vertexOffset] = operation.layerSlot;
118
+ commandSlots[vertexOffset] = operation.commandSlot;
119
+ vertexOffset += 1;
120
+ }
121
+ }
122
+ return {
123
+ kind,
124
+ resource,
125
+ maskResource,
126
+ blendMode,
127
+ compositeOperation,
128
+ operations: [...operations],
129
+ commandIds: operations.map((operation) => operation.command.id),
130
+ vertices,
131
+ layerSlots,
132
+ commandSlots,
133
+ };
134
+ }
135
+
136
+ function commandBlendMode(value: unknown): SemanticBlendMode {
137
+ if (value == null) return "src_over";
138
+ if (value === "src_over" || value === "src_in" || value === "dst_in" || value === "multiply" || value === "screen" || value === "add") return value;
139
+ throw new Error(`unsupported semantic blend mode ${String(value)}`);
140
+ }
141
+
142
+ function requireCompositeOperation(value: unknown, commandId: string): SemanticCompositeOperation {
143
+ if (value == null || value === "marker") return "marker";
144
+ if (value === "begin_isolation" || value === "end_isolation") return value;
145
+ throw new Error(`unsupported composite operation ${commandId}: ${String(value)}`);
146
+ }
147
+
148
+ function commandClip(
149
+ value: unknown,
150
+ baseMatrix: [number, number, number, number, number, number],
151
+ commandId: string
152
+ ): [[number, number], [number, number], [number, number], [number, number]] {
153
+ if (value == null) return [[-1e9, -1e9], [1e9, -1e9], [1e9, 1e9], [-1e9, 1e9]];
154
+ if (!Array.isArray(value) || value.length !== 4 || value.some((point) => !Array.isArray(point) || point.length !== 2 || point.some((entry) => typeof entry !== "number" || !Number.isFinite(entry)))) {
155
+ throw new Error(`invalid command clip ${commandId}`);
156
+ }
157
+ return value.map((point) => transformPoint(baseMatrix, point[0], point[1])) as [[number, number], [number, number], [number, number], [number, number]];
158
+ }
159
+
160
+ function gradientColor(value: unknown, x: number, y: number, fallback: number[]): number[] {
161
+ if (!value || typeof value !== "object" || Array.isArray(value)) return fallback;
162
+ const gradient = value as Record<string, unknown>;
163
+ const start = optionalPoint(gradient.start, [0, 0.5]);
164
+ const end = optionalPoint(gradient.end, [1, 0.5]);
165
+ const startColor = optionalColor(gradient.start_color, fallback);
166
+ const endColor = optionalColor(gradient.end_color, fallback);
167
+ const dx = end[0] - start[0];
168
+ const dy = end[1] - start[1];
169
+ const lengthSq = dx * dx + dy * dy;
170
+ const t = lengthSq > 1e-9 ? Math.max(0, Math.min(1, ((x - start[0]) * dx + (y - start[1]) * dy) / lengthSq)) : 0;
171
+ return startColor.map((component, index) => component + (endColor[index] - component) * t);
172
+ }
173
+
174
+ function optionalPoint(value: unknown, fallback: [number, number]): [number, number] {
175
+ return Array.isArray(value) && value.length === 2 && value.every((entry) => typeof entry === "number" && Number.isFinite(entry))
176
+ ? [value[0], value[1]]
177
+ : fallback;
178
+ }
179
+
180
+ function transformPoint(matrix: [number, number, number, number, number, number], x: number, y: number): [number, number] {
181
+ return [matrix[0] * x + matrix[2] * y + matrix[4], matrix[1] * x + matrix[3] * y + matrix[5]];
182
+ }
183
+
184
+ function requireMatrix(value: unknown, commandId: string): [number, number, number, number, number, number] {
185
+ if (!Array.isArray(value) || value.length !== 6 || value.some((entry) => typeof entry !== "number" || !Number.isFinite(entry))) {
186
+ throw new Error(`invalid command matrix ${commandId}`);
187
+ }
188
+ return value as [number, number, number, number, number, number];
189
+ }
190
+
191
+ function requireRect(value: unknown, commandId: string): { x: number; y: number; width: number; height: number } {
192
+ const rect = optionalRect(value);
193
+ if (![rect.x, rect.y, rect.width, rect.height].every(Number.isFinite) || rect.width < 0 || rect.height < 0) {
194
+ throw new Error(`invalid command bounds ${commandId}`);
195
+ }
196
+ return rect;
197
+ }
198
+
199
+ function optionalRect(value: unknown): { x: number; y: number; width: number; height: number } {
200
+ if (!value || typeof value !== "object" || Array.isArray(value)) return { x: 0, y: 0, width: 1, height: 1 };
201
+ const rect = value as Record<string, unknown>;
202
+ return {
203
+ x: Number(rect.x ?? 0),
204
+ y: Number(rect.y ?? 0),
205
+ width: Number(rect.width ?? 1),
206
+ height: Number(rect.height ?? 1),
207
+ };
208
+ }
209
+
210
+ function optionalColor(value: unknown, fallback: number[]): number[] {
211
+ return Array.isArray(value) && value.length === 4 && value.every((entry) => typeof entry === "number" && Number.isFinite(entry))
212
+ ? value
213
+ : fallback;
214
+ }
215
+
216
+ function shapeParams(value: unknown, _bounds: { width: number; height: number }): [number, number, number] {
217
+ if (value === "ellipse") return [2, 0, 0];
218
+ if (value === "rect") return [0, 0, 0];
219
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [0, 0, 0];
220
+ const rounded = (value as Record<string, unknown>).rounded_rect;
221
+ if (rounded && typeof rounded === "object" && !Array.isArray(rounded)) {
222
+ const radius = (rounded as Record<string, unknown>).radius;
223
+ if (Array.isArray(radius) && radius.length === 2) {
224
+ return [1, Number(radius[0]), Number(radius[1])];
225
+ }
226
+ }
227
+ return [0, 0, 0];
228
+ }
229
+
230
+ function imageClipParams(value: unknown, _bounds: { width: number; height: number }): [number, number, number] {
231
+ if (value === "ellipse") return [2, 0, 0];
232
+ if (!value || typeof value !== "object" || Array.isArray(value)) return [0, 0, 0];
233
+ const rounded = (value as Record<string, unknown>).rounded_rect;
234
+ if (rounded && typeof rounded === "object" && !Array.isArray(rounded)) {
235
+ const radius = (rounded as Record<string, unknown>).radius;
236
+ if (Array.isArray(radius) && radius.length === 2) {
237
+ return [1, Number(radius[0]), Number(radius[1])];
238
+ }
239
+ }
240
+ return [0, 0, 0];
241
+ }
242
+
243
+ function assetMaskResource(value: unknown): SemanticResourceKey | null {
244
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
245
+ const mask = (value as Record<string, unknown>).asset_mask;
246
+ if (!mask || typeof mask !== "object" || Array.isArray(mask)) return null;
247
+ return requireResource((mask as Record<string, unknown>).resource, "asset-mask");
248
+ }
249
+
250
+ function requireResource(value: unknown, commandId: string): SemanticResourceKey {
251
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`missing command resource ${commandId}`);
252
+ const resource = value as Record<string, unknown>;
253
+ if (typeof resource.namespace !== "string" || typeof resource.key !== "string") throw new Error(`invalid command resource ${commandId}`);
254
+ return { namespace: resource.namespace, key: resource.key };
255
+ }
256
+
257
+ function optionalResource(value: unknown): SemanticResourceKey | null {
258
+ if (value == null) return null;
259
+ return requireResource(value, "image-alpha-mask");
260
+ }
@@ -0,0 +1,280 @@
1
+ export type SemanticResourceKey = { namespace: string; key: string };
2
+
3
+ export type SemanticCommand = {
4
+ id: string;
5
+ layer_id: string;
6
+ role: string;
7
+ payload: Record<string, unknown> & { kind: string };
8
+ [key: string]: unknown;
9
+ };
10
+
11
+ export type SemanticLayerPatch = {
12
+ layer_id: string;
13
+ render_mask: boolean | null;
14
+ transform: { dx: number; dy: number } | null;
15
+ };
16
+
17
+ export type SemanticCommandStatePatch = {
18
+ slot: number;
19
+ render_mask: boolean | null;
20
+ transform: { dx: number; dy: number } | null;
21
+ };
22
+
23
+ export type SemanticCommandPlanInput = {
24
+ layerTableRevision: number;
25
+ layerTable: Array<{
26
+ layer_id: string;
27
+ parent_id: string | null;
28
+ slot: number;
29
+ subtree_start: number;
30
+ subtree_end: number;
31
+ }>;
32
+ layerSources: Array<{
33
+ layer_id: string;
34
+ matrix: [number, number, number, number, number, number];
35
+ }>;
36
+ layerCommands: Array<{
37
+ layer_id: string;
38
+ render_mask: boolean;
39
+ transform: { dx: number; dy: number };
40
+ command_start: number;
41
+ command_count: number;
42
+ }>;
43
+ semanticCommands: SemanticCommand[];
44
+ commandStates?: Array<{
45
+ command_id: string;
46
+ slot: number;
47
+ render_mask: boolean;
48
+ transform: { dx: number; dy: number };
49
+ }>;
50
+ };
51
+
52
+ export type SemanticDrawOperation = {
53
+ command: SemanticCommand;
54
+ layerId: string;
55
+ layerSlot: number;
56
+ baseMatrix: [number, number, number, number, number, number];
57
+ visible: boolean;
58
+ transform: { dx: number; dy: number };
59
+ commandSlot: number;
60
+ commandVisible: boolean;
61
+ commandTransform: { dx: number; dy: number };
62
+ };
63
+
64
+ type LayerRuntimeState = {
65
+ slot: number;
66
+ baseMatrix: [number, number, number, number, number, number];
67
+ visible: boolean;
68
+ transform: { dx: number; dy: number };
69
+ };
70
+
71
+ /**
72
+ * Immutable authored-layer command ownership plus mutable render state.
73
+ * Visibility and dynamic deltas update only the dense state table; command
74
+ * order and resource identity remain stable for the plan lifetime.
75
+ */
76
+ export class SemanticCommandPlan {
77
+ readonly layerTableRevision: number;
78
+ readonly commandRevision = 1;
79
+ readonly resourceRevision = 1;
80
+ private readonly commands: SemanticCommand[];
81
+ private readonly operationLayers: Array<{ layerId: string; slot: number }>;
82
+ private readonly stateByLayer = new Map<string, LayerRuntimeState>();
83
+ private readonly resources: SemanticResourceKey[];
84
+ private readonly commandVisible: boolean[];
85
+ private readonly commandTransforms: Array<{ dx: number; dy: number }>;
86
+
87
+ constructor(input: SemanticCommandPlanInput) {
88
+ this.layerTableRevision = input.layerTableRevision;
89
+ this.commands = [...input.semanticCommands];
90
+ const stateBySlot = input.commandStates ?? this.commands.map((command, slot) => ({
91
+ command_id: command.id,
92
+ slot,
93
+ render_mask: true,
94
+ transform: { dx: 0, dy: 0 },
95
+ }));
96
+ if (stateBySlot.length !== this.commands.length) throw new Error("command state table length mismatch");
97
+ this.commandVisible = new Array(this.commands.length);
98
+ this.commandTransforms = new Array(this.commands.length);
99
+ for (const state of stateBySlot) {
100
+ if (!Number.isInteger(state.slot) || state.slot < 0 || state.slot >= this.commands.length || this.commands[state.slot].id !== state.command_id) {
101
+ throw new Error(`command state identity mismatch at ${state.slot}`);
102
+ }
103
+ this.commandVisible[state.slot] = state.render_mask;
104
+ this.commandTransforms[state.slot] = { ...state.transform };
105
+ }
106
+ const tableByLayer = new Map<string, number>();
107
+ const matrixByLayer = new Map(input.layerSources.map((layer) => [layer.layer_id, layer.matrix] as const));
108
+ for (const entry of input.layerTable) {
109
+ if (tableByLayer.has(entry.layer_id)) throw new Error(`duplicate layer table id ${entry.layer_id}`);
110
+ if (!Number.isInteger(entry.slot) || entry.slot < 0) throw new Error(`invalid layer slot ${entry.slot}`);
111
+ tableByLayer.set(entry.layer_id, entry.slot);
112
+ }
113
+ const coverage = new Uint8Array(this.commands.length);
114
+ this.operationLayers = new Array(this.commands.length);
115
+ for (const layerCommand of input.layerCommands) {
116
+ const slot = tableByLayer.get(layerCommand.layer_id);
117
+ if (slot == null) throw new Error(`layer command references unknown layer ${layerCommand.layer_id}`);
118
+ if (this.stateByLayer.has(layerCommand.layer_id)) {
119
+ throw new Error(`duplicate layer command ${layerCommand.layer_id}`);
120
+ }
121
+ const baseMatrix = matrixByLayer.get(layerCommand.layer_id);
122
+ if (!baseMatrix) throw new Error(`missing authored layer matrix ${layerCommand.layer_id}`);
123
+ const start = layerCommand.command_start;
124
+ const end = start + layerCommand.command_count;
125
+ if (!Number.isInteger(start) || !Number.isInteger(end) || start < 0 || end > this.commands.length) {
126
+ throw new Error(`invalid command span for layer ${layerCommand.layer_id}`);
127
+ }
128
+ for (let index = start; index < end; index += 1) {
129
+ const command = this.commands[index];
130
+ if (coverage[index] !== 0 || command.layer_id !== layerCommand.layer_id) {
131
+ throw new Error(`command span ownership mismatch at ${index}`);
132
+ }
133
+ coverage[index] = 1;
134
+ this.operationLayers[index] = { layerId: layerCommand.layer_id, slot };
135
+ }
136
+ this.stateByLayer.set(layerCommand.layer_id, {
137
+ slot,
138
+ baseMatrix: [...baseMatrix] as LayerRuntimeState["baseMatrix"],
139
+ visible: layerCommand.render_mask,
140
+ transform: { ...layerCommand.transform },
141
+ });
142
+ }
143
+ const uncovered = coverage.findIndex((value) => value === 0);
144
+ if (uncovered >= 0) throw new Error(`semantic command ${uncovered} is outside every authored layer span`);
145
+ this.resources = collectResourceRequests(this.commands);
146
+ }
147
+
148
+ operations(): SemanticDrawOperation[] {
149
+ return this.commands.map((command, index) => {
150
+ const ownership = this.operationLayers[index];
151
+ const state = this.stateByLayer.get(ownership.layerId);
152
+ if (!state) throw new Error(`missing runtime layer state ${ownership.layerId}`);
153
+ return {
154
+ command,
155
+ layerId: ownership.layerId,
156
+ layerSlot: ownership.slot,
157
+ baseMatrix: [...state.baseMatrix] as SemanticDrawOperation["baseMatrix"],
158
+ visible: state.visible,
159
+ transform: { ...state.transform },
160
+ commandSlot: index,
161
+ commandVisible: this.commandVisible[index],
162
+ commandTransform: { ...this.commandTransforms[index] },
163
+ };
164
+ });
165
+ }
166
+
167
+ resourceRequests(): SemanticResourceKey[] {
168
+ return this.resources.map((resource) => ({ ...resource }));
169
+ }
170
+
171
+ applyLayerPatches(patches: SemanticLayerPatch[]): void {
172
+ for (const patch of patches) {
173
+ const state = this.stateByLayer.get(patch.layer_id);
174
+ if (!state) throw new Error(`unknown layer patch ${patch.layer_id}`);
175
+ if (patch.render_mask != null) state.visible = patch.render_mask;
176
+ if (patch.transform != null) state.transform = { ...patch.transform };
177
+ }
178
+ }
179
+
180
+ applyCommandPatches(patches: SemanticCommandStatePatch[]): void {
181
+ for (const patch of patches) {
182
+ if (!Number.isInteger(patch.slot) || patch.slot < 0 || patch.slot >= this.commands.length) throw new Error(`invalid command slot ${patch.slot}`);
183
+ if (patch.render_mask != null) this.commandVisible[patch.slot] = patch.render_mask;
184
+ if (patch.transform != null) this.commandTransforms[patch.slot] = { ...patch.transform };
185
+ }
186
+ }
187
+ }
188
+
189
+ export function semanticCommandPlanFromCoreSnapshot(snapshot: {
190
+ revisions: { layer_table: number } & Record<string, number>;
191
+ layer_table: SemanticCommandPlanInput["layerTable"];
192
+ layer_sources: Array<{ id: string; matrix: [number, number, number, number, number, number] }>;
193
+ commands: SemanticCommandPlanInput["layerCommands"];
194
+ semantic_commands: SemanticCommand[];
195
+ command_states: NonNullable<SemanticCommandPlanInput["commandStates"]>;
196
+ }): SemanticCommandPlan {
197
+ return new SemanticCommandPlan({
198
+ layerTableRevision: snapshot.revisions.layer_table,
199
+ layerTable: snapshot.layer_table,
200
+ layerSources: snapshot.layer_sources.map((layer) => ({ layer_id: layer.id, matrix: layer.matrix })),
201
+ layerCommands: snapshot.commands,
202
+ semanticCommands: snapshot.semantic_commands,
203
+ commandStates: snapshot.command_states,
204
+ });
205
+ }
206
+
207
+ export type CoreSceneDumpInput = {
208
+ revisions: { layer_table: number } & Record<string, number>;
209
+ layer_table: SemanticCommandPlanInput["layerTable"];
210
+ layers: Array<{
211
+ layer_id: string;
212
+ matrix: [number, number, number, number, number, number];
213
+ render_mask: boolean;
214
+ commands: SemanticCommand[];
215
+ }>;
216
+ command_states: NonNullable<SemanticCommandPlanInput["commandStates"]>;
217
+ };
218
+
219
+ /** Adapts the native/server debug dump to the exact same immutable browser
220
+ * command plan used by a live WASM Scene. This is intentionally strict so a
221
+ * dump cannot silently reorder authored layers or command ownership. */
222
+ export function semanticCommandPlanFromCoreDump(dump: CoreSceneDumpInput): SemanticCommandPlan {
223
+ const layerById = new Map(dump.layers.map((layer) => [layer.layer_id, layer] as const));
224
+ const semanticCommands: SemanticCommand[] = [];
225
+ const layerCommands: SemanticCommandPlanInput["layerCommands"] = [];
226
+ const layerSources: SemanticCommandPlanInput["layerSources"] = [];
227
+ for (const entry of [...dump.layer_table].sort((left, right) => left.slot - right.slot)) {
228
+ const layer = layerById.get(entry.layer_id);
229
+ if (!layer) throw new Error(`core dump missing layer ${entry.layer_id}`);
230
+ const commandStart = semanticCommands.length;
231
+ semanticCommands.push(...layer.commands);
232
+ layerSources.push({ layer_id: layer.layer_id, matrix: layer.matrix });
233
+ layerCommands.push({
234
+ layer_id: layer.layer_id,
235
+ render_mask: layer.render_mask,
236
+ transform: { dx: 0, dy: 0 },
237
+ command_start: commandStart,
238
+ command_count: layer.commands.length,
239
+ });
240
+ }
241
+ if (layerById.size !== dump.layer_table.length) throw new Error("core dump contains layers outside the layer table");
242
+ return new SemanticCommandPlan({
243
+ layerTableRevision: dump.revisions.layer_table,
244
+ layerTable: dump.layer_table,
245
+ layerSources,
246
+ layerCommands,
247
+ semanticCommands,
248
+ commandStates: dump.command_states,
249
+ });
250
+ }
251
+
252
+ function collectResourceRequests(commands: SemanticCommand[]): SemanticResourceKey[] {
253
+ const deduplicated = new Map<string, SemanticResourceKey>();
254
+ for (const command of commands) {
255
+ const resource = commandResource(command.payload);
256
+ if (!resource) continue;
257
+ deduplicated.set(`${resource.namespace}\0${resource.key}`, resource);
258
+ if (command.payload.kind === "image") {
259
+ const mask = asResourceKey(command.payload.alpha_mask);
260
+ if (mask) deduplicated.set(`${mask.namespace}\0${mask.key}`, mask);
261
+ }
262
+ }
263
+ return [...deduplicated.values()].map((resource) => ({ ...resource }));
264
+ }
265
+
266
+ function commandResource(payload: SemanticCommand["payload"]): SemanticResourceKey | null {
267
+ if (payload.kind === "image") return asResourceKey(payload.resource);
268
+ if (payload.kind !== "shape") return null;
269
+ const primitive = payload.primitive;
270
+ if (!primitive || typeof primitive !== "object" || Array.isArray(primitive)) return null;
271
+ const assetMask = (primitive as Record<string, unknown>).asset_mask;
272
+ if (!assetMask || typeof assetMask !== "object" || Array.isArray(assetMask)) return null;
273
+ return asResourceKey((assetMask as Record<string, unknown>).resource);
274
+ }
275
+
276
+ function asResourceKey(value: unknown): SemanticResourceKey | null {
277
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
278
+ const { namespace, key } = value as Record<string, unknown>;
279
+ return typeof namespace === "string" && typeof key === "string" ? { namespace, key } : null;
280
+ }
@@ -0,0 +1,35 @@
1
+ import type { GlyphInstance } from "../types/glyph.js";
2
+ import { buildSdfGlyphInstanceVertices } from "./webglSdfGlyphPipeline.js";
3
+ import type { SemanticDrawOperation } from "./semanticCommandPlanner.js";
4
+
5
+ export function compileSemanticTextGlyphBatches(
6
+ instances: GlyphInstance[],
7
+ textOperations: SemanticDrawOperation[]
8
+ ): Map<string, Float32Array> {
9
+ const result = new Map<string, Float32Array>();
10
+ for (const operation of textOperations) {
11
+ if (operation.command.payload.kind !== "text") throw new Error(`non-text glyph bridge operation ${operation.command.id}`);
12
+ const commandInstances = instances.filter((instance) => instance.layerId === operation.command.id);
13
+ const clips = operation.command.clip == null
14
+ ? new Map<string, [[number, number], [number, number], [number, number], [number, number]]>()
15
+ : new Map([[operation.command.id, transformClip(operation.command.clip, operation.baseMatrix, operation.command.id)]]);
16
+ result.set(
17
+ operation.command.id,
18
+ buildSdfGlyphInstanceVertices(
19
+ commandInstances,
20
+ new Map([[operation.command.id, operation.layerSlot]]),
21
+ clips,
22
+ new Map([[operation.command.id, operation.commandSlot]])
23
+ )
24
+ );
25
+ }
26
+ return result;
27
+ }
28
+
29
+ function transformClip(value: unknown, matrix: [number, number, number, number, number, number], commandId: string): [[number, number], [number, number], [number, number], [number, number]] {
30
+ if (!Array.isArray(value) || value.length !== 4 || value.some((point) => !Array.isArray(point) || point.length !== 2)) throw new Error(`invalid text clip ${commandId}`);
31
+ return value.map((point) => [
32
+ matrix[0] * Number(point[0]) + matrix[2] * Number(point[1]) + matrix[4],
33
+ matrix[1] * Number(point[0]) + matrix[3] * Number(point[1]) + matrix[5],
34
+ ]) as [[number, number], [number, number], [number, number], [number, number]];
35
+ }