@beyondwork/docx-react-component 1.0.109 → 1.0.111

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 (59) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +3 -0
  3. package/src/model/layout/page-graph-types.ts +33 -0
  4. package/src/model/layout/runtime-page-graph-types.ts +25 -0
  5. package/src/runtime/document-runtime.ts +46 -0
  6. package/src/runtime/geometry/adjacent-geometry-intake.ts +820 -15
  7. package/src/runtime/geometry/caret-geometry.ts +219 -7
  8. package/src/runtime/geometry/geometry-index.ts +52 -12
  9. package/src/runtime/geometry/object-handles.ts +42 -1
  10. package/src/runtime/layout/index.ts +3 -0
  11. package/src/runtime/layout/inert-layout-facet.ts +13 -0
  12. package/src/runtime/layout/layout-engine-instance.ts +233 -4
  13. package/src/runtime/layout/layout-engine-version.ts +47 -2
  14. package/src/runtime/layout/layout-facet-types.ts +3 -0
  15. package/src/runtime/layout/page-graph.ts +88 -7
  16. package/src/runtime/layout/paginated-layout-engine.ts +34 -0
  17. package/src/runtime/layout/project-block-fragments.ts +144 -1
  18. package/src/runtime/layout/public-facet.ts +228 -9
  19. package/src/runtime/layout/resolve-page-previews.ts +46 -8
  20. package/src/runtime/scopes/adjacent-geometry-evidence.ts +456 -0
  21. package/src/runtime/scopes/compile-scope-bundle.ts +8 -0
  22. package/src/runtime/scopes/evidence.ts +16 -0
  23. package/src/runtime/scopes/index.ts +13 -0
  24. package/src/runtime/scopes/semantic-scope-types.ts +67 -0
  25. package/src/ui-tailwind/chrome-overlay/tw-table-split-row-carry-overlay.tsx +62 -0
  26. package/src/ui-tailwind/debug/layer11-consumer-readiness.ts +104 -0
  27. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +50 -5
  28. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +27 -0
  29. package/src/ui-tailwind/page-stack/tw-page-chrome-entry.tsx +62 -0
  30. package/src/ui-tailwind/page-stack/tw-page-stack-chrome-layer.tsx +1 -0
  31. package/src/README.md +0 -85
  32. package/src/api/README.md +0 -26
  33. package/src/api/v3/README.md +0 -91
  34. package/src/component-inventory.md +0 -99
  35. package/src/core/README.md +0 -10
  36. package/src/core/commands/README.md +0 -3
  37. package/src/core/schema/README.md +0 -3
  38. package/src/core/selection/README.md +0 -3
  39. package/src/core/state/README.md +0 -3
  40. package/src/io/README.md +0 -10
  41. package/src/io/export/README.md +0 -3
  42. package/src/io/normalize/README.md +0 -3
  43. package/src/io/ooxml/README.md +0 -3
  44. package/src/io/opc/README.md +0 -3
  45. package/src/model/README.md +0 -3
  46. package/src/preservation/README.md +0 -3
  47. package/src/review/README.md +0 -16
  48. package/src/review/store/README.md +0 -3
  49. package/src/runtime/README.md +0 -3
  50. package/src/ui/README.md +0 -30
  51. package/src/ui/comments/README.md +0 -3
  52. package/src/ui/compatibility/README.md +0 -3
  53. package/src/ui/editor-surface/README.md +0 -3
  54. package/src/ui/review/README.md +0 -3
  55. package/src/ui/status/README.md +0 -3
  56. package/src/ui/theme/README.md +0 -3
  57. package/src/ui/toolbar/README.md +0 -3
  58. package/src/ui-tailwind/debug/README.md +0 -22
  59. package/src/validation/README.md +0 -3
@@ -20,20 +20,116 @@ export interface AdjacentGeometryTwipsRect {
20
20
  readonly heightTwips?: number;
21
21
  }
22
22
 
23
+ export interface AdjacentGeometryFramePxRect {
24
+ readonly leftPx: number;
25
+ readonly topPx: number;
26
+ readonly widthPx: number;
27
+ readonly heightPx: number;
28
+ readonly coordinateSpace: "frame-px";
29
+ }
30
+
31
+ export interface AdjacentGeometryFramePxPoint {
32
+ readonly xPx: number;
33
+ readonly yPx: number;
34
+ readonly coordinateSpace: "frame-px";
35
+ }
36
+
23
37
  export interface AdjacentGeometryPageSpan {
24
38
  readonly startPage?: number | null;
25
39
  readonly endPage?: number | null;
26
40
  }
27
41
 
42
+ export type AdjacentGeometryPageIndexStatus =
43
+ | "matches-runtime"
44
+ | "differs-from-runtime"
45
+ | "runtime-missing"
46
+ | "word-page-missing";
47
+
48
+ export interface AdjacentGeometryL04FrameProjection {
49
+ readonly pageIndex?: number;
50
+ readonly pageId?: string;
51
+ readonly frameId?: string;
52
+ readonly frameCompleteness?: string;
53
+ readonly physicalBoundsTwips?: AdjacentGeometryTwipsRect;
54
+ readonly framePixelScale?: AdjacentGeometryFramePixelScale;
55
+ readonly markerLane?: AdjacentGeometryTwipsRect;
56
+ readonly textColumn?: AdjacentGeometryTwipsRect;
57
+ readonly fieldStartAnchorTwips?: {
58
+ readonly x?: number;
59
+ readonly y?: number;
60
+ };
61
+ readonly fieldEndAnchorTwips?: {
62
+ readonly x?: number;
63
+ readonly y?: number;
64
+ };
65
+ readonly coordinateSpace: "l04-frame-twips";
66
+ readonly precision: "word-page-local-calibration";
67
+ readonly status:
68
+ | "projected-l04-frame"
69
+ | "page-index-differs-from-runtime"
70
+ | "runtime-page-missing"
71
+ | "word-page-missing"
72
+ | "missing-l04-page-frame"
73
+ | "missing-word-page-local-geometry";
74
+ }
75
+
76
+ export interface AdjacentGeometryFramePixelScale {
77
+ readonly source: "runtime-render-frame-page-rect";
78
+ readonly pageIndex: number;
79
+ readonly pageId?: string;
80
+ readonly frameId?: string;
81
+ readonly renderFrameRevision?: number;
82
+ readonly renderPageFrame: AdjacentGeometryFramePxRect;
83
+ readonly physicalBoundsTwips: AdjacentGeometryTwipsRect;
84
+ readonly pxPerTwipX: number;
85
+ readonly pxPerTwipY: number;
86
+ readonly zoomPxPerTwip?: number;
87
+ }
88
+
89
+ export interface AdjacentGeometryFramePixelProjection {
90
+ readonly pageIndex?: number;
91
+ readonly pageId?: string;
92
+ readonly frameId?: string;
93
+ readonly scale?: AdjacentGeometryFramePixelScale;
94
+ readonly markerLane?: AdjacentGeometryFramePxRect;
95
+ readonly textColumn?: AdjacentGeometryFramePxRect;
96
+ readonly fieldStartAnchorPx?: AdjacentGeometryFramePxPoint;
97
+ readonly fieldEndAnchorPx?: AdjacentGeometryFramePxPoint;
98
+ readonly fieldResultRangePx?: AdjacentGeometryFramePxRect;
99
+ readonly coordinateSpace: "frame-px";
100
+ readonly precision: "word-page-local-calibration";
101
+ readonly status:
102
+ | "projected-frame-pixels"
103
+ | "awaiting-l04-frame-projection"
104
+ | "missing-frame-pixel-scale"
105
+ | "missing-l04-frame-rect"
106
+ | "missing-l04-field-anchor";
107
+ }
108
+
109
+ interface AdjacentGeometryPageFrame {
110
+ readonly pageId?: string;
111
+ readonly pageIndex: number;
112
+ readonly frameId?: string;
113
+ readonly completeness?: string;
114
+ readonly physicalBoundsTwips?: AdjacentGeometryTwipsRect;
115
+ readonly framePixelScale?: AdjacentGeometryFramePixelScale;
116
+ }
117
+
28
118
  export interface AdjacentGeometryPageLocalNumberingGeometry {
29
119
  readonly wordPage?: number;
120
+ readonly normalizedWordPageIndex?: number;
30
121
  readonly runtimePageIndex?: number;
122
+ readonly pageIndexStatus: AdjacentGeometryPageIndexStatus;
31
123
  readonly runtimeFragmentId?: string;
32
124
  readonly markerLane?: AdjacentGeometryTwipsRect;
33
125
  readonly textColumn?: AdjacentGeometryTwipsRect;
126
+ readonly l04FrameProjection: AdjacentGeometryL04FrameProjection;
127
+ readonly framePixelProjection: AdjacentGeometryFramePixelProjection;
128
+ readonly compositorReady: boolean;
34
129
  readonly coordinateSpace: "word-page-twips";
35
130
  readonly precision: "word-page-local-calibration";
36
131
  readonly normalizationStatus:
132
+ | "l04-frame-twips-projected"
37
133
  | "word-page-local-awaiting-l04-frame-projection"
38
134
  | "missing-word-page-local-lane";
39
135
  }
@@ -56,9 +152,13 @@ export interface AdjacentGeometryPageLocalFieldGeometry {
56
152
  readonly x?: number;
57
153
  readonly y?: number;
58
154
  };
155
+ readonly l04FrameProjection: AdjacentGeometryL04FrameProjection;
156
+ readonly framePixelProjection: AdjacentGeometryFramePixelProjection;
157
+ readonly compositorReady: boolean;
59
158
  readonly coordinateSpace: "word-page-twips";
60
159
  readonly precision: "word-page-local-calibration";
61
160
  readonly normalizationStatus:
161
+ | "l04-frame-twips-projected"
62
162
  | "word-page-local-awaiting-l04-frame-projection"
63
163
  | "missing-word-placement";
64
164
  }
@@ -69,6 +169,9 @@ export interface AdjacentGeometryNumberingRowSummary {
69
169
  readonly canonicalBlockId?: string;
70
170
  readonly runtimeBlockId?: string;
71
171
  readonly runtimeFragmentId?: string;
172
+ readonly runtimeNumberingId?: string;
173
+ readonly layoutObjectId?: string;
174
+ readonly runtimeJoinSource: "adjacent-live" | "l04-numbering-readback";
72
175
  readonly runtimePageIndex?: number;
73
176
  readonly sourceParagraphPath?: string;
74
177
  readonly canonicalNumberingInstanceId?: string;
@@ -94,6 +197,7 @@ export interface AdjacentGeometryNumberingRowSummary {
94
197
  readonly heightTwips?: number;
95
198
  };
96
199
  readonly pageLocalGeometry: AdjacentGeometryPageLocalNumberingGeometry;
200
+ readonly compositorReady: boolean;
97
201
  readonly rangeMappingMethod?: string;
98
202
  readonly coordinateSpace: "screen-px";
99
203
  readonly precision: "screen-space-approximate";
@@ -116,6 +220,7 @@ export interface AdjacentGeometryFieldRegionRowSummary {
116
220
  readonly endPage?: number | null;
117
221
  };
118
222
  readonly pageLocalGeometry: AdjacentGeometryPageLocalFieldGeometry;
223
+ readonly compositorReady: boolean;
119
224
  readonly instructionFamily?: string;
120
225
  readonly resultLineRectCount: number;
121
226
  readonly wordPlacement?: {
@@ -141,6 +246,7 @@ export interface AdjacentGeometryDocumentSummary {
141
246
  readonly docId: string;
142
247
  readonly numberingRows: number;
143
248
  readonly numberingRowsWithRuntimeFragment: number;
249
+ readonly numberingRowsJoinedFromL04Readback: number;
144
250
  readonly numberingParagraphLineRects: number;
145
251
  readonly numberingTextOnlyLineRects: number;
146
252
  readonly numberingRawMarkerRectRows: number;
@@ -150,6 +256,13 @@ export interface AdjacentGeometryDocumentSummary {
150
256
  readonly numberingRowsWithUiaCapture: number;
151
257
  readonly numberingRowsWithPageLocalMarkerLane: number;
152
258
  readonly numberingRowsWithPageLocalTextColumn: number;
259
+ readonly numberingRowsWithRuntimePageMatch: number;
260
+ readonly numberingUiaRowsWithRuntimePageMatch: number;
261
+ readonly numberingRowsWithL04FrameProjection: number;
262
+ readonly numberingUiaRowsWithL04FrameProjection: number;
263
+ readonly numberingRowsWithFramePixelProjection: number;
264
+ readonly numberingUiaRowsWithFramePixelProjection: number;
265
+ readonly numberingCompositorReadyRows: number;
153
266
  readonly fieldRegionRows: number;
154
267
  readonly fieldRegionRowsWithRuntimeId: number;
155
268
  readonly fieldRegionResultLineRects: number;
@@ -157,10 +270,15 @@ export interface AdjacentGeometryDocumentSummary {
157
270
  readonly fieldRegionRowsWithWordPlacement: number;
158
271
  readonly fieldRegionRowsWithRuntimePageMatch: number;
159
272
  readonly fieldRegionRowsWithRuntimePageContainment: number;
273
+ readonly fieldRegionRowsWithL04FrameProjection: number;
274
+ readonly fieldRegionUiaRowsWithL04FrameProjection: number;
275
+ readonly fieldRegionRowsWithFramePixelProjection: number;
276
+ readonly fieldRegionUiaRowsWithFramePixelProjection: number;
277
+ readonly fieldRegionCompositorReadyRows: number;
160
278
  }
161
279
 
162
280
  export interface AdjacentGeometryIntakeSummary {
163
- readonly schemaVersion: "layer-05-adjacent-geometry-intake/v0";
281
+ readonly schemaVersion: "layer-05-adjacent-geometry-intake/v2";
164
282
  readonly generatedAtUtc: string;
165
283
  readonly sourceRuns: {
166
284
  readonly adjacentLiveWordJoins: string;
@@ -170,6 +288,7 @@ export interface AdjacentGeometryIntakeSummary {
170
288
  readonly totals: {
171
289
  readonly numberingRows: number;
172
290
  readonly numberingRowsWithRuntimeFragment: number;
291
+ readonly numberingRowsJoinedFromL04Readback: number;
173
292
  readonly numberingParagraphLineRects: number;
174
293
  readonly numberingTextOnlyLineRects: number;
175
294
  readonly numberingRawMarkerRectRows: number;
@@ -179,6 +298,13 @@ export interface AdjacentGeometryIntakeSummary {
179
298
  readonly numberingRowsWithUiaCapture: number;
180
299
  readonly numberingRowsWithPageLocalMarkerLane: number;
181
300
  readonly numberingRowsWithPageLocalTextColumn: number;
301
+ readonly numberingRowsWithRuntimePageMatch: number;
302
+ readonly numberingUiaRowsWithRuntimePageMatch: number;
303
+ readonly numberingRowsWithL04FrameProjection: number;
304
+ readonly numberingUiaRowsWithL04FrameProjection: number;
305
+ readonly numberingRowsWithFramePixelProjection: number;
306
+ readonly numberingUiaRowsWithFramePixelProjection: number;
307
+ readonly numberingCompositorReadyRows: number;
182
308
  readonly fieldRegionRows: number;
183
309
  readonly fieldRegionRowsWithRuntimeId: number;
184
310
  readonly fieldRegionResultLineRects: number;
@@ -186,6 +312,11 @@ export interface AdjacentGeometryIntakeSummary {
186
312
  readonly fieldRegionRowsWithWordPlacement: number;
187
313
  readonly fieldRegionRowsWithRuntimePageMatch: number;
188
314
  readonly fieldRegionRowsWithRuntimePageContainment: number;
315
+ readonly fieldRegionRowsWithL04FrameProjection: number;
316
+ readonly fieldRegionUiaRowsWithL04FrameProjection: number;
317
+ readonly fieldRegionRowsWithFramePixelProjection: number;
318
+ readonly fieldRegionUiaRowsWithFramePixelProjection: number;
319
+ readonly fieldRegionCompositorReadyRows: number;
189
320
  };
190
321
  readonly routing: {
191
322
  readonly routedTo: "L05";
@@ -198,7 +329,12 @@ export interface AdjacentGeometryIntakeSummary {
198
329
  readonly coordinateSpace: "word-page-twips";
199
330
  readonly precision: "word-page-local-calibration";
200
331
  readonly requiredUpstreamFact: "L04 frame-to-pixel projection";
201
- readonly compositorReady: false;
332
+ readonly l04FrameProjectionCoordinateSpace: "l04-frame-twips";
333
+ readonly l04FrameProjectionPrecision: "word-page-local-calibration";
334
+ readonly framePixelCoordinateSpace: "frame-px";
335
+ readonly framePixelPrecision: "word-page-local-calibration";
336
+ readonly scaleSource: "runtime-render-frame-page-rect";
337
+ readonly compositorReady: boolean;
202
338
  };
203
339
  readonly documents: readonly AdjacentGeometryDocumentSummary[];
204
340
  readonly numberingRows: readonly AdjacentGeometryNumberingRowSummary[];
@@ -210,16 +346,23 @@ export function summarizeAdjacentGeometryIntake(input: {
210
346
  readonly sourceRuns: AdjacentGeometryIntakeSummary["sourceRuns"];
211
347
  readonly numberingArtifact: unknown;
212
348
  readonly numberingLiveJoinArtifacts: readonly unknown[];
349
+ readonly numberingRuntimeReadbackArtifacts?: readonly unknown[];
213
350
  readonly fieldRegionRuntimeReadback: unknown;
214
351
  readonly fieldRegionLiveJoinArtifacts: readonly unknown[];
215
352
  }): AdjacentGeometryIntakeSummary {
353
+ const pageFramesByDocId = collectPageFramesByDocId(
354
+ input.numberingRuntimeReadbackArtifacts ?? [],
355
+ );
216
356
  const numberingRows = summarizeNumberingRows(
217
357
  input.numberingArtifact,
218
358
  input.numberingLiveJoinArtifacts,
359
+ input.numberingRuntimeReadbackArtifacts ?? [],
360
+ pageFramesByDocId,
219
361
  );
220
362
  const fieldRegionRows = summarizeFieldRegionRows(
221
363
  input.fieldRegionRuntimeReadback,
222
364
  input.fieldRegionLiveJoinArtifacts,
365
+ pageFramesByDocId,
223
366
  );
224
367
  const docIds = [
225
368
  ...new Set([
@@ -228,7 +371,7 @@ export function summarizeAdjacentGeometryIntake(input: {
228
371
  ]),
229
372
  ].sort();
230
373
  return {
231
- schemaVersion: "layer-05-adjacent-geometry-intake/v0",
374
+ schemaVersion: "layer-05-adjacent-geometry-intake/v2",
232
375
  generatedAtUtc: input.generatedAtUtc,
233
376
  sourceRuns: input.sourceRuns,
234
377
  totals: {
@@ -236,6 +379,9 @@ export function summarizeAdjacentGeometryIntake(input: {
236
379
  numberingRowsWithRuntimeFragment: numberingRows.filter(
237
380
  (row) => row.runtimeFragmentId !== undefined,
238
381
  ).length,
382
+ numberingRowsJoinedFromL04Readback: numberingRows.filter(
383
+ (row) => row.runtimeJoinSource === "l04-numbering-readback",
384
+ ).length,
239
385
  numberingParagraphLineRects: sum(
240
386
  numberingRows,
241
387
  (row) => row.paragraphLineRectCount,
@@ -258,6 +404,28 @@ export function summarizeAdjacentGeometryIntake(input: {
258
404
  numberingRowsWithPageLocalTextColumn: numberingRows.filter(
259
405
  (row) => row.pageLocalGeometry.textColumn !== undefined,
260
406
  ).length,
407
+ numberingRowsWithRuntimePageMatch: numberingRows.filter(
408
+ (row) => row.pageLocalGeometry.pageIndexStatus === "matches-runtime",
409
+ ).length,
410
+ numberingUiaRowsWithRuntimePageMatch: numberingRows.filter(
411
+ (row) =>
412
+ hasNumberingUiaCapture(row) &&
413
+ row.pageLocalGeometry.pageIndexStatus === "matches-runtime",
414
+ ).length,
415
+ numberingRowsWithL04FrameProjection: numberingRows.filter(hasL04FrameProjection)
416
+ .length,
417
+ numberingUiaRowsWithL04FrameProjection: numberingRows.filter(
418
+ (row) => hasNumberingUiaCapture(row) && hasL04FrameProjection(row),
419
+ ).length,
420
+ numberingRowsWithFramePixelProjection: numberingRows.filter(
421
+ hasFramePixelProjection,
422
+ ).length,
423
+ numberingUiaRowsWithFramePixelProjection: numberingRows.filter(
424
+ (row) => hasNumberingUiaCapture(row) && hasFramePixelProjection(row),
425
+ ).length,
426
+ numberingCompositorReadyRows: numberingRows.filter(
427
+ (row) => row.pageLocalGeometry.compositorReady,
428
+ ).length,
261
429
  fieldRegionRows: fieldRegionRows.length,
262
430
  fieldRegionRowsWithRuntimeId: fieldRegionRows.filter(
263
431
  (row) => row.runtimeFieldRegionId !== undefined,
@@ -278,6 +446,21 @@ export function summarizeAdjacentGeometryIntake(input: {
278
446
  fieldRegionRowsWithRuntimePageContainment: fieldRegionRows.filter(
279
447
  (row) => row.pageLocalGeometry.pageSpanStatus === "contained-in-runtime",
280
448
  ).length,
449
+ fieldRegionRowsWithL04FrameProjection: fieldRegionRows.filter(
450
+ hasL04FrameProjection,
451
+ ).length,
452
+ fieldRegionUiaRowsWithL04FrameProjection: fieldRegionRows.filter(
453
+ (row) => row.resultLineRectCount > 0 && hasL04FrameProjection(row),
454
+ ).length,
455
+ fieldRegionRowsWithFramePixelProjection: fieldRegionRows.filter(
456
+ hasFramePixelProjection,
457
+ ).length,
458
+ fieldRegionUiaRowsWithFramePixelProjection: fieldRegionRows.filter(
459
+ (row) => row.resultLineRectCount > 0 && hasFramePixelProjection(row),
460
+ ).length,
461
+ fieldRegionCompositorReadyRows: fieldRegionRows.filter(
462
+ (row) => row.pageLocalGeometry.compositorReady,
463
+ ).length,
281
464
  },
282
465
  routing: {
283
466
  routedTo: "L05",
@@ -290,7 +473,14 @@ export function summarizeAdjacentGeometryIntake(input: {
290
473
  coordinateSpace: "word-page-twips",
291
474
  precision: "word-page-local-calibration",
292
475
  requiredUpstreamFact: "L04 frame-to-pixel projection",
293
- compositorReady: false,
476
+ l04FrameProjectionCoordinateSpace: "l04-frame-twips",
477
+ l04FrameProjectionPrecision: "word-page-local-calibration",
478
+ framePixelCoordinateSpace: "frame-px",
479
+ framePixelPrecision: "word-page-local-calibration",
480
+ scaleSource: "runtime-render-frame-page-rect",
481
+ compositorReady:
482
+ numberingRows.some((row) => row.pageLocalGeometry.compositorReady) ||
483
+ fieldRegionRows.some((row) => row.pageLocalGeometry.compositorReady),
294
484
  },
295
485
  documents: docIds.map((docId) =>
296
486
  summarizeDocument(
@@ -307,6 +497,8 @@ export function summarizeAdjacentGeometryIntake(input: {
307
497
  function summarizeNumberingRows(
308
498
  artifact: unknown,
309
499
  liveJoinArtifacts: readonly unknown[],
500
+ runtimeReadbackArtifacts: readonly unknown[],
501
+ pageFramesByDocId: ReadonlyMap<string, ReadonlyMap<number, AdjacentGeometryPageFrame>>,
310
502
  ): readonly AdjacentGeometryNumberingRowSummary[] {
311
503
  const uiaRowsByKey = new Map<string, Record<string, unknown>>();
312
504
  for (const raw of array(record(artifact).rows)) {
@@ -322,8 +514,16 @@ function summarizeNumberingRows(
322
514
  const liveRows = liveJoinArtifacts.flatMap((artifact) =>
323
515
  array(record(artifact).numberingRows).map(record),
324
516
  );
517
+ const runtimeReadbacksByKey = collectNumberingRuntimeReadbacks(
518
+ runtimeReadbackArtifacts,
519
+ );
325
520
  const rows = liveRows.length > 0
326
- ? liveRows.filter((row) => stringValue(row.runtimeFragmentId) !== undefined)
521
+ ? liveRows.filter((row) => {
522
+ if (stringValue(row.runtimeFragmentId) !== undefined) return true;
523
+ return (
524
+ numberingRuntimeReadbackForRow(row, runtimeReadbacksByKey) !== undefined
525
+ );
526
+ })
327
527
  : array(record(artifact).rows).map(record);
328
528
 
329
529
  return rows
@@ -338,6 +538,10 @@ function summarizeNumberingRows(
338
538
  const uiaRow =
339
539
  uiaRowsByKey.get(numberingKey(docId, wordParagraphOrdinalGlobal) ?? "") ??
340
540
  {};
541
+ const runtimeReadback = numberingRuntimeReadbackForRow(
542
+ row,
543
+ runtimeReadbacksByKey,
544
+ );
341
545
  const uiaTargetJoin = record(uiaRow.targetJoin);
342
546
  const paragraphLineRects = chooseRects(
343
547
  uiaRow.paragraphLineRects,
@@ -350,28 +554,63 @@ function summarizeNumberingRows(
350
554
  const rawMarkerRects = chooseRawArray(uiaRow.markerRects, row.markerRects);
351
555
  const markerRects = chooseRects(uiaRow.markerRects, row.markerRects);
352
556
  const markerLane =
353
- normalizeTwipsRect(row.markerLane) ?? normalizeTwipsRect(uiaTargetJoin.markerLane);
557
+ normalizeTwipsRect(row.markerLane) ??
558
+ normalizeTwipsRect(runtimeReadback?.markerLane) ??
559
+ normalizeTwipsRect(uiaTargetJoin.markerLane);
354
560
  const textColumn =
355
- normalizeTwipsRect(row.textColumn) ?? normalizeTwipsRect(uiaTargetJoin.textColumn);
561
+ normalizeTwipsRect(row.textColumn) ??
562
+ normalizeTwipsRect(runtimeReadback?.textColumn) ??
563
+ normalizeTwipsRect(uiaTargetJoin.textColumn);
356
564
  const runtimeFragmentId =
357
- stringValue(row.runtimeFragmentId) ?? stringValue(uiaTargetJoin.runtimeFragmentId);
565
+ stringValue(row.runtimeFragmentId) ??
566
+ stringValue(runtimeReadback?.runtimeFragmentId) ??
567
+ stringValue(uiaTargetJoin.runtimeFragmentId);
568
+ const runtimeJoinSource: AdjacentGeometryNumberingRowSummary["runtimeJoinSource"] =
569
+ stringValue(row.runtimeFragmentId) !== undefined
570
+ ? "adjacent-live"
571
+ : "l04-numbering-readback";
358
572
  const runtimePageIndex =
359
- numberValue(row.runtimePageIndex) ?? numberValue(uiaTargetJoin.runtimePageIndex);
573
+ numberValue(row.runtimePageIndex) ??
574
+ numberValue(runtimeReadback?.pageIndex) ??
575
+ numberValue(uiaTargetJoin.runtimePageIndex);
360
576
  const wordStartPage =
361
577
  numberValue(record(row.pageSpan).startPage) ??
362
578
  numberValue(row.wordStartPage) ??
363
579
  numberValue(uiaRow.wordStartPage);
580
+ const normalizedWordPageIndex =
581
+ wordStartPage === undefined ? undefined : wordStartPage - 1;
582
+ const pageIndexStatus = classifyPageIndex(
583
+ normalizedWordPageIndex,
584
+ runtimePageIndex,
585
+ );
586
+ const l04FrameProjection = projectNumberingToL04Frame({
587
+ docId,
588
+ normalizedWordPageIndex,
589
+ runtimePageIndex,
590
+ pageIndexStatus,
591
+ markerLane,
592
+ textColumn,
593
+ pageFramesByDocId,
594
+ });
595
+ const framePixelProjection = projectNumberingToFramePixels(l04FrameProjection);
364
596
  const pageLocalGeometry: AdjacentGeometryPageLocalNumberingGeometry = {
365
597
  wordPage: wordStartPage,
598
+ normalizedWordPageIndex,
366
599
  runtimePageIndex,
600
+ pageIndexStatus,
367
601
  runtimeFragmentId,
368
602
  markerLane,
369
603
  textColumn,
604
+ l04FrameProjection,
605
+ framePixelProjection,
606
+ compositorReady: framePixelProjection.status === "projected-frame-pixels",
370
607
  coordinateSpace: "word-page-twips",
371
608
  precision: "word-page-local-calibration",
372
609
  normalizationStatus:
373
- markerLane || textColumn
374
- ? "word-page-local-awaiting-l04-frame-projection"
610
+ l04FrameProjection.status === "projected-l04-frame"
611
+ ? "l04-frame-twips-projected"
612
+ : markerLane || textColumn
613
+ ? "word-page-local-awaiting-l04-frame-projection"
375
614
  : "missing-word-page-local-lane",
376
615
  };
377
616
  return {
@@ -380,11 +619,20 @@ function summarizeNumberingRows(
380
619
  canonicalBlockId:
381
620
  stringValue(row.canonicalBlockId) ?? stringValue(uiaTargetJoin.canonicalBlockId),
382
621
  runtimeBlockId:
383
- stringValue(row.runtimeBlockId) ?? stringValue(uiaTargetJoin.runtimeBlockId),
622
+ stringValue(row.runtimeBlockId) ??
623
+ stringValue(runtimeReadback?.numberingSourceBlockId) ??
624
+ stringValue(runtimeReadback?.sourceBlockId) ??
625
+ stringValue(uiaTargetJoin.runtimeBlockId),
384
626
  runtimeFragmentId,
627
+ runtimeNumberingId:
628
+ stringValue(runtimeReadback?.runtimeNumberingId) ??
629
+ stringValue(record(row.runtimeJoinAttempt).runtimeNumberingId),
630
+ layoutObjectId: stringValue(runtimeReadback?.layoutObjectId),
631
+ runtimeJoinSource,
385
632
  runtimePageIndex,
386
633
  sourceParagraphPath:
387
634
  stringValue(row.sourceParagraphPath) ??
635
+ stringValue(runtimeReadback?.sourceBlockPath) ??
388
636
  stringValue(uiaTargetJoin.sourceParagraphPath),
389
637
  canonicalNumberingInstanceId:
390
638
  stringValue(row.canonicalNumberingInstanceId) ??
@@ -403,6 +651,7 @@ function summarizeNumberingRows(
403
651
  markerLane,
404
652
  textColumn,
405
653
  pageLocalGeometry,
654
+ compositorReady: pageLocalGeometry.compositorReady,
406
655
  rangeMappingMethod:
407
656
  stringValue(uiaRow.rangeMappingMethod) ?? stringValue(row.rangeMappingMethod),
408
657
  coordinateSpace: "screen-px" as const,
@@ -423,6 +672,7 @@ function summarizeNumberingRows(
423
672
  function summarizeFieldRegionRows(
424
673
  runtimeReadback: unknown,
425
674
  liveJoinArtifacts: readonly unknown[],
675
+ pageFramesByDocId: ReadonlyMap<string, ReadonlyMap<number, AdjacentGeometryPageFrame>>,
426
676
  ): readonly AdjacentGeometryFieldRegionRowSummary[] {
427
677
  const liveJoinByOrdinalKey = new Map<string, Record<string, unknown>>();
428
678
  const uniqueLiveJoinRows: Record<string, unknown>[] = [];
@@ -500,18 +750,33 @@ function summarizeFieldRegionRows(
500
750
  const wordPlacement = normalizeWordPlacement(
501
751
  record(readbackRow.wordResultPlacement),
502
752
  );
753
+ const pageSpanStatus = classifyPageSpan(normalizedWordPageIndexSpan, runtimePageSpan);
754
+ const l04FrameProjection = projectFieldToL04Frame({
755
+ docId,
756
+ normalizedWordPageIndexSpan,
757
+ runtimePageSpan,
758
+ pageSpanStatus,
759
+ wordPlacement,
760
+ pageFramesByDocId,
761
+ });
762
+ const framePixelProjection = projectFieldToFramePixels(l04FrameProjection);
503
763
  const pageLocalGeometry: AdjacentGeometryPageLocalFieldGeometry = {
504
764
  expectedPageSpan,
505
765
  normalizedWordPageIndexSpan,
506
766
  runtimePageSpan,
507
- pageSpanStatus: classifyPageSpan(normalizedWordPageIndexSpan, runtimePageSpan),
767
+ pageSpanStatus,
508
768
  startAnchorTwips: wordPlacement?.startXYTwips,
509
769
  endAnchorTwips: wordPlacement?.endXYTwips,
770
+ l04FrameProjection,
771
+ framePixelProjection,
772
+ compositorReady: framePixelProjection.status === "projected-frame-pixels",
510
773
  coordinateSpace: "word-page-twips",
511
774
  precision: "word-page-local-calibration",
512
775
  normalizationStatus:
513
- wordPlacement?.startXYTwips || wordPlacement?.endXYTwips
514
- ? "word-page-local-awaiting-l04-frame-projection"
776
+ l04FrameProjection.status === "projected-l04-frame"
777
+ ? "l04-frame-twips-projected"
778
+ : wordPlacement?.startXYTwips || wordPlacement?.endXYTwips
779
+ ? "word-page-local-awaiting-l04-frame-projection"
515
780
  : "missing-word-placement",
516
781
  };
517
782
  return {
@@ -534,6 +799,7 @@ function summarizeFieldRegionRows(
534
799
  runtimePageSpan,
535
800
  expectedPageSpan,
536
801
  pageLocalGeometry,
802
+ compositorReady: pageLocalGeometry.compositorReady,
537
803
  instructionFamily:
538
804
  stringValue(liveJoin.instructionFamily) ??
539
805
  stringValue(targetJoin.instructionFamily),
@@ -562,6 +828,9 @@ function summarizeDocument(
562
828
  numberingRowsWithRuntimeFragment: numberingRows.filter(
563
829
  (row) => row.runtimeFragmentId !== undefined,
564
830
  ).length,
831
+ numberingRowsJoinedFromL04Readback: numberingRows.filter(
832
+ (row) => row.runtimeJoinSource === "l04-numbering-readback",
833
+ ).length,
565
834
  numberingParagraphLineRects: sum(
566
835
  numberingRows,
567
836
  (row) => row.paragraphLineRectCount,
@@ -584,6 +853,27 @@ function summarizeDocument(
584
853
  numberingRowsWithPageLocalTextColumn: numberingRows.filter(
585
854
  (row) => row.pageLocalGeometry.textColumn !== undefined,
586
855
  ).length,
856
+ numberingRowsWithRuntimePageMatch: numberingRows.filter(
857
+ (row) => row.pageLocalGeometry.pageIndexStatus === "matches-runtime",
858
+ ).length,
859
+ numberingUiaRowsWithRuntimePageMatch: numberingRows.filter(
860
+ (row) =>
861
+ hasNumberingUiaCapture(row) &&
862
+ row.pageLocalGeometry.pageIndexStatus === "matches-runtime",
863
+ ).length,
864
+ numberingRowsWithL04FrameProjection: numberingRows.filter(hasL04FrameProjection)
865
+ .length,
866
+ numberingUiaRowsWithL04FrameProjection: numberingRows.filter(
867
+ (row) => hasNumberingUiaCapture(row) && hasL04FrameProjection(row),
868
+ ).length,
869
+ numberingRowsWithFramePixelProjection: numberingRows.filter(
870
+ hasFramePixelProjection,
871
+ ).length,
872
+ numberingUiaRowsWithFramePixelProjection: numberingRows.filter(
873
+ (row) => hasNumberingUiaCapture(row) && hasFramePixelProjection(row),
874
+ ).length,
875
+ numberingCompositorReadyRows: numberingRows.filter((row) => row.compositorReady)
876
+ .length,
587
877
  fieldRegionRows: fieldRegionRows.length,
588
878
  fieldRegionRowsWithRuntimeId: fieldRegionRows.filter(
589
879
  (row) => row.runtimeFieldRegionId !== undefined,
@@ -604,9 +894,35 @@ function summarizeDocument(
604
894
  fieldRegionRowsWithRuntimePageContainment: fieldRegionRows.filter(
605
895
  (row) => row.pageLocalGeometry.pageSpanStatus === "contained-in-runtime",
606
896
  ).length,
897
+ fieldRegionRowsWithL04FrameProjection: fieldRegionRows.filter(
898
+ hasL04FrameProjection,
899
+ ).length,
900
+ fieldRegionUiaRowsWithL04FrameProjection: fieldRegionRows.filter(
901
+ (row) => row.resultLineRectCount > 0 && hasL04FrameProjection(row),
902
+ ).length,
903
+ fieldRegionRowsWithFramePixelProjection: fieldRegionRows.filter(
904
+ hasFramePixelProjection,
905
+ ).length,
906
+ fieldRegionUiaRowsWithFramePixelProjection: fieldRegionRows.filter(
907
+ (row) => row.resultLineRectCount > 0 && hasFramePixelProjection(row),
908
+ ).length,
909
+ fieldRegionCompositorReadyRows: fieldRegionRows.filter((row) => row.compositorReady)
910
+ .length,
607
911
  };
608
912
  }
609
913
 
914
+ function hasL04FrameProjection(
915
+ row: AdjacentGeometryNumberingRowSummary | AdjacentGeometryFieldRegionRowSummary,
916
+ ): boolean {
917
+ return row.pageLocalGeometry.l04FrameProjection.status === "projected-l04-frame";
918
+ }
919
+
920
+ function hasFramePixelProjection(
921
+ row: AdjacentGeometryNumberingRowSummary | AdjacentGeometryFieldRegionRowSummary,
922
+ ): boolean {
923
+ return row.pageLocalGeometry.framePixelProjection.status === "projected-frame-pixels";
924
+ }
925
+
610
926
  function hasNumberingUiaCapture(row: AdjacentGeometryNumberingRowSummary): boolean {
611
927
  return (
612
928
  row.paragraphLineRectCount > 0 ||
@@ -615,6 +931,462 @@ function hasNumberingUiaCapture(row: AdjacentGeometryNumberingRowSummary): boole
615
931
  );
616
932
  }
617
933
 
934
+ function collectPageFramesByDocId(
935
+ artifacts: readonly unknown[],
936
+ ): Map<string, Map<number, AdjacentGeometryPageFrame>> {
937
+ const byDocId = new Map<string, Map<number, AdjacentGeometryPageFrame>>();
938
+ for (const artifact of artifacts) {
939
+ const artifactRecord = record(artifact);
940
+ const docId = stringValue(artifactRecord.docId);
941
+ if (!docId) continue;
942
+ for (const raw of array(artifactRecord.pages)) {
943
+ const page = record(raw);
944
+ const pageIndex = numberValue(page.pageIndex);
945
+ if (pageIndex === undefined) continue;
946
+ const frame = record(page.frame);
947
+ const physicalBoundsTwips = normalizeTwipsRect(frame.physicalBoundsTwips);
948
+ const renderFrame = record(page.renderFrame);
949
+ const renderPageFrame = normalizeFramePxRect(renderFrame.frame);
950
+ const renderFrameRevision = numberValue(renderFrame.revision);
951
+ const zoomPxPerTwip =
952
+ numberValue(record(page.renderZoom).pxPerTwip) ??
953
+ numberValue(renderFrame.zoomPxPerTwip);
954
+ const frameId = stringValue(frame.frameId);
955
+ const pageFrame: AdjacentGeometryPageFrame = {
956
+ pageId: stringValue(page.pageId),
957
+ pageIndex,
958
+ frameId,
959
+ completeness: stringValue(frame.completeness),
960
+ physicalBoundsTwips,
961
+ framePixelScale: buildFramePixelScale({
962
+ pageIndex,
963
+ pageId: stringValue(page.pageId),
964
+ frameId,
965
+ physicalBoundsTwips,
966
+ renderPageFrame,
967
+ renderFrameRevision,
968
+ zoomPxPerTwip,
969
+ }),
970
+ };
971
+ let docFrames = byDocId.get(docId);
972
+ if (!docFrames) {
973
+ docFrames = new Map<number, AdjacentGeometryPageFrame>();
974
+ byDocId.set(docId, docFrames);
975
+ }
976
+ if (!docFrames.has(pageIndex)) docFrames.set(pageIndex, pageFrame);
977
+ }
978
+ }
979
+ return byDocId;
980
+ }
981
+
982
+ function buildFramePixelScale(input: {
983
+ readonly pageIndex: number;
984
+ readonly pageId?: string;
985
+ readonly frameId?: string;
986
+ readonly physicalBoundsTwips?: AdjacentGeometryTwipsRect;
987
+ readonly renderPageFrame?: AdjacentGeometryFramePxRect;
988
+ readonly renderFrameRevision?: number;
989
+ readonly zoomPxPerTwip?: number;
990
+ }): AdjacentGeometryFramePixelScale | undefined {
991
+ const physicalBounds = input.physicalBoundsTwips;
992
+ const renderPageFrame = input.renderPageFrame;
993
+ if (!physicalBounds || !renderPageFrame) return undefined;
994
+ if (
995
+ physicalBounds.xTwips === undefined ||
996
+ physicalBounds.yTwips === undefined ||
997
+ physicalBounds.widthTwips === undefined ||
998
+ physicalBounds.heightTwips === undefined ||
999
+ physicalBounds.widthTwips <= 0 ||
1000
+ physicalBounds.heightTwips <= 0
1001
+ ) {
1002
+ return undefined;
1003
+ }
1004
+ const pxPerTwipX = renderPageFrame.widthPx / physicalBounds.widthTwips;
1005
+ const pxPerTwipY = renderPageFrame.heightPx / physicalBounds.heightTwips;
1006
+ if (!isFinitePositive(pxPerTwipX) || !isFinitePositive(pxPerTwipY)) {
1007
+ return undefined;
1008
+ }
1009
+ return {
1010
+ source: "runtime-render-frame-page-rect",
1011
+ pageIndex: input.pageIndex,
1012
+ pageId: input.pageId,
1013
+ frameId: input.frameId,
1014
+ renderFrameRevision: input.renderFrameRevision,
1015
+ renderPageFrame,
1016
+ physicalBoundsTwips: physicalBounds,
1017
+ pxPerTwipX,
1018
+ pxPerTwipY,
1019
+ zoomPxPerTwip:
1020
+ input.zoomPxPerTwip === undefined ? undefined : roundNumber(input.zoomPxPerTwip),
1021
+ };
1022
+ }
1023
+
1024
+ function collectNumberingRuntimeReadbacks(
1025
+ artifacts: readonly unknown[],
1026
+ ): Map<string, Record<string, unknown>> {
1027
+ const byKey = new Map<string, Record<string, unknown>>();
1028
+ for (const artifact of artifacts) {
1029
+ const artifactRecord = record(artifact);
1030
+ const docId = stringValue(artifactRecord.docId);
1031
+ for (const raw of array(artifactRecord.numberingFragments)) {
1032
+ const row = record(raw);
1033
+ const key = numberingRuntimeReadbackKey(
1034
+ docId ?? stringValue(row.docId),
1035
+ stringValue(row.numberingKey) ?? stringValue(row.runtimeNumberingId),
1036
+ stringValue(row.numberingInstanceId),
1037
+ numberValue(row.level),
1038
+ );
1039
+ if (key && !byKey.has(key)) byKey.set(key, row);
1040
+ }
1041
+ }
1042
+ return byKey;
1043
+ }
1044
+
1045
+ function numberingRuntimeReadbackForRow(
1046
+ row: Record<string, unknown>,
1047
+ runtimeReadbacksByKey: ReadonlyMap<string, Record<string, unknown>>,
1048
+ ): Record<string, unknown> | undefined {
1049
+ const key = numberingRuntimeReadbackKey(
1050
+ stringValue(row.docId),
1051
+ stringValue(record(row.canonicalJoinAttempt).numberingKey),
1052
+ stringValue(row.canonicalNumberingInstanceId),
1053
+ numberValue(row.canonicalLevel),
1054
+ );
1055
+ return key ? runtimeReadbacksByKey.get(key) : undefined;
1056
+ }
1057
+
1058
+ function projectNumberingToL04Frame(input: {
1059
+ readonly docId: string;
1060
+ readonly normalizedWordPageIndex: number | undefined;
1061
+ readonly runtimePageIndex: number | undefined;
1062
+ readonly pageIndexStatus: AdjacentGeometryPageIndexStatus;
1063
+ readonly markerLane: AdjacentGeometryTwipsRect | undefined;
1064
+ readonly textColumn: AdjacentGeometryTwipsRect | undefined;
1065
+ readonly pageFramesByDocId: ReadonlyMap<string, ReadonlyMap<number, AdjacentGeometryPageFrame>>;
1066
+ }): AdjacentGeometryL04FrameProjection {
1067
+ if (input.pageIndexStatus === "word-page-missing") {
1068
+ return baseFrameProjection("word-page-missing");
1069
+ }
1070
+ if (input.pageIndexStatus === "runtime-missing") {
1071
+ return baseFrameProjection("runtime-page-missing", input.normalizedWordPageIndex);
1072
+ }
1073
+ if (input.pageIndexStatus === "differs-from-runtime") {
1074
+ return baseFrameProjection(
1075
+ "page-index-differs-from-runtime",
1076
+ input.normalizedWordPageIndex,
1077
+ );
1078
+ }
1079
+ if (!input.markerLane && !input.textColumn) {
1080
+ return baseFrameProjection(
1081
+ "missing-word-page-local-geometry",
1082
+ input.normalizedWordPageIndex,
1083
+ );
1084
+ }
1085
+ const frame = pageFrameFor(input.pageFramesByDocId, input.docId, input.runtimePageIndex);
1086
+ if (!frame?.physicalBoundsTwips) {
1087
+ return baseFrameProjection("missing-l04-page-frame", input.runtimePageIndex);
1088
+ }
1089
+ return {
1090
+ pageIndex: input.runtimePageIndex,
1091
+ pageId: frame.pageId,
1092
+ frameId: frame.frameId,
1093
+ frameCompleteness: frame.completeness,
1094
+ physicalBoundsTwips: frame.physicalBoundsTwips,
1095
+ framePixelScale: frame.framePixelScale,
1096
+ markerLane: addFrameOrigin(input.markerLane, frame.physicalBoundsTwips),
1097
+ textColumn: addFrameOrigin(input.textColumn, frame.physicalBoundsTwips),
1098
+ coordinateSpace: "l04-frame-twips",
1099
+ precision: "word-page-local-calibration",
1100
+ status: "projected-l04-frame",
1101
+ };
1102
+ }
1103
+
1104
+ function projectFieldToL04Frame(input: {
1105
+ readonly docId: string;
1106
+ readonly normalizedWordPageIndexSpan: AdjacentGeometryPageSpan | undefined;
1107
+ readonly runtimePageSpan: AdjacentGeometryPageSpan | undefined;
1108
+ readonly pageSpanStatus: AdjacentGeometryPageLocalFieldGeometry["pageSpanStatus"];
1109
+ readonly wordPlacement:
1110
+ | AdjacentGeometryFieldRegionRowSummary["wordPlacement"]
1111
+ | undefined;
1112
+ readonly pageFramesByDocId: ReadonlyMap<string, ReadonlyMap<number, AdjacentGeometryPageFrame>>;
1113
+ }): AdjacentGeometryL04FrameProjection {
1114
+ const normalizedStart = input.normalizedWordPageIndexSpan?.startPage;
1115
+ if (normalizedStart == null) return baseFrameProjection("word-page-missing");
1116
+ if (!input.runtimePageSpan || input.runtimePageSpan.startPage == null) {
1117
+ return baseFrameProjection("runtime-page-missing", normalizedStart);
1118
+ }
1119
+ if (
1120
+ input.pageSpanStatus !== "matches-runtime" &&
1121
+ input.pageSpanStatus !== "contained-in-runtime"
1122
+ ) {
1123
+ return baseFrameProjection("page-index-differs-from-runtime", normalizedStart);
1124
+ }
1125
+ if (!input.wordPlacement?.startXYTwips && !input.wordPlacement?.endXYTwips) {
1126
+ return baseFrameProjection("missing-word-page-local-geometry", normalizedStart);
1127
+ }
1128
+ const frame = pageFrameFor(
1129
+ input.pageFramesByDocId,
1130
+ input.docId,
1131
+ input.runtimePageSpan.startPage,
1132
+ );
1133
+ if (!frame?.physicalBoundsTwips) {
1134
+ return baseFrameProjection("missing-l04-page-frame", input.runtimePageSpan.startPage);
1135
+ }
1136
+ return {
1137
+ pageIndex: input.runtimePageSpan.startPage,
1138
+ pageId: frame.pageId,
1139
+ frameId: frame.frameId,
1140
+ frameCompleteness: frame.completeness,
1141
+ physicalBoundsTwips: frame.physicalBoundsTwips,
1142
+ framePixelScale: frame.framePixelScale,
1143
+ fieldStartAnchorTwips: addFrameOriginToPoint(
1144
+ input.wordPlacement.startXYTwips,
1145
+ frame.physicalBoundsTwips,
1146
+ ),
1147
+ fieldEndAnchorTwips: addFrameOriginToPoint(
1148
+ input.wordPlacement.endXYTwips,
1149
+ frame.physicalBoundsTwips,
1150
+ ),
1151
+ coordinateSpace: "l04-frame-twips",
1152
+ precision: "word-page-local-calibration",
1153
+ status: "projected-l04-frame",
1154
+ };
1155
+ }
1156
+
1157
+ function projectNumberingToFramePixels(
1158
+ projection: AdjacentGeometryL04FrameProjection,
1159
+ ): AdjacentGeometryFramePixelProjection {
1160
+ if (projection.status !== "projected-l04-frame") {
1161
+ return baseFramePixelProjection("awaiting-l04-frame-projection", projection);
1162
+ }
1163
+ const scale = projectionScale(projection);
1164
+ if (!scale) return baseFramePixelProjection("missing-frame-pixel-scale", projection);
1165
+ const markerLane = l04FrameTwipsRectToFramePx(projection.markerLane, scale);
1166
+ const textColumn = l04FrameTwipsRectToFramePx(projection.textColumn, scale);
1167
+ if (!markerLane) {
1168
+ return baseFramePixelProjection("missing-l04-frame-rect", projection, scale);
1169
+ }
1170
+ return {
1171
+ pageIndex: projection.pageIndex,
1172
+ pageId: projection.pageId,
1173
+ frameId: projection.frameId,
1174
+ scale,
1175
+ markerLane,
1176
+ textColumn,
1177
+ coordinateSpace: "frame-px",
1178
+ precision: "word-page-local-calibration",
1179
+ status: "projected-frame-pixels",
1180
+ };
1181
+ }
1182
+
1183
+ function projectFieldToFramePixels(
1184
+ projection: AdjacentGeometryL04FrameProjection,
1185
+ ): AdjacentGeometryFramePixelProjection {
1186
+ if (projection.status !== "projected-l04-frame") {
1187
+ return baseFramePixelProjection("awaiting-l04-frame-projection", projection);
1188
+ }
1189
+ const scale = projectionScale(projection);
1190
+ if (!scale) return baseFramePixelProjection("missing-frame-pixel-scale", projection);
1191
+ const fieldStartAnchorPx = l04FrameTwipsPointToFramePx(
1192
+ projection.fieldStartAnchorTwips,
1193
+ scale,
1194
+ );
1195
+ const fieldEndAnchorPx = l04FrameTwipsPointToFramePx(
1196
+ projection.fieldEndAnchorTwips,
1197
+ scale,
1198
+ );
1199
+ if (!fieldStartAnchorPx || !fieldEndAnchorPx) {
1200
+ return baseFramePixelProjection("missing-l04-field-anchor", projection, scale);
1201
+ }
1202
+ return {
1203
+ pageIndex: projection.pageIndex,
1204
+ pageId: projection.pageId,
1205
+ frameId: projection.frameId,
1206
+ scale,
1207
+ fieldStartAnchorPx,
1208
+ fieldEndAnchorPx,
1209
+ fieldResultRangePx: rangeRectFromPoints(fieldStartAnchorPx, fieldEndAnchorPx),
1210
+ coordinateSpace: "frame-px",
1211
+ precision: "word-page-local-calibration",
1212
+ status: "projected-frame-pixels",
1213
+ };
1214
+ }
1215
+
1216
+ function baseFrameProjection(
1217
+ status: AdjacentGeometryL04FrameProjection["status"],
1218
+ pageIndex?: number,
1219
+ ): AdjacentGeometryL04FrameProjection {
1220
+ return {
1221
+ pageIndex,
1222
+ coordinateSpace: "l04-frame-twips",
1223
+ precision: "word-page-local-calibration",
1224
+ status,
1225
+ };
1226
+ }
1227
+
1228
+ function baseFramePixelProjection(
1229
+ status: AdjacentGeometryFramePixelProjection["status"],
1230
+ projection: AdjacentGeometryL04FrameProjection,
1231
+ scale?: AdjacentGeometryFramePixelScale,
1232
+ ): AdjacentGeometryFramePixelProjection {
1233
+ return {
1234
+ pageIndex: projection.pageIndex,
1235
+ pageId: projection.pageId,
1236
+ frameId: projection.frameId,
1237
+ scale,
1238
+ coordinateSpace: "frame-px",
1239
+ precision: "word-page-local-calibration",
1240
+ status,
1241
+ };
1242
+ }
1243
+
1244
+ function pageFrameFor(
1245
+ pageFramesByDocId: ReadonlyMap<string, ReadonlyMap<number, AdjacentGeometryPageFrame>>,
1246
+ docId: string,
1247
+ pageIndex: number | undefined,
1248
+ ): AdjacentGeometryPageFrame | undefined {
1249
+ return pageIndex === undefined ? undefined : pageFramesByDocId.get(docId)?.get(pageIndex);
1250
+ }
1251
+
1252
+ function projectionScale(
1253
+ projection: AdjacentGeometryL04FrameProjection,
1254
+ ): AdjacentGeometryFramePixelScale | undefined {
1255
+ return projection.framePixelScale;
1256
+ }
1257
+
1258
+ function addFrameOrigin(
1259
+ rect: AdjacentGeometryTwipsRect | undefined,
1260
+ frame: AdjacentGeometryTwipsRect,
1261
+ ): AdjacentGeometryTwipsRect | undefined {
1262
+ if (!rect) return undefined;
1263
+ return {
1264
+ xTwips:
1265
+ rect.xTwips === undefined || frame.xTwips === undefined
1266
+ ? rect.xTwips
1267
+ : rect.xTwips + frame.xTwips,
1268
+ yTwips:
1269
+ rect.yTwips === undefined || frame.yTwips === undefined
1270
+ ? rect.yTwips
1271
+ : rect.yTwips + frame.yTwips,
1272
+ widthTwips: rect.widthTwips,
1273
+ heightTwips: rect.heightTwips,
1274
+ };
1275
+ }
1276
+
1277
+ function addFrameOriginToPoint(
1278
+ point: { readonly x?: number; readonly y?: number } | undefined,
1279
+ frame: AdjacentGeometryTwipsRect,
1280
+ ): { readonly x?: number; readonly y?: number } | undefined {
1281
+ if (!point) return undefined;
1282
+ return {
1283
+ x:
1284
+ point.x === undefined || frame.xTwips === undefined
1285
+ ? point.x
1286
+ : point.x + frame.xTwips,
1287
+ y:
1288
+ point.y === undefined || frame.yTwips === undefined
1289
+ ? point.y
1290
+ : point.y + frame.yTwips,
1291
+ };
1292
+ }
1293
+
1294
+ function l04FrameTwipsRectToFramePx(
1295
+ rect: AdjacentGeometryTwipsRect | undefined,
1296
+ scale: AdjacentGeometryFramePixelScale,
1297
+ ): AdjacentGeometryFramePxRect | undefined {
1298
+ if (
1299
+ !rect ||
1300
+ rect.xTwips === undefined ||
1301
+ rect.yTwips === undefined ||
1302
+ rect.widthTwips === undefined ||
1303
+ rect.heightTwips === undefined ||
1304
+ scale.physicalBoundsTwips.xTwips === undefined ||
1305
+ scale.physicalBoundsTwips.yTwips === undefined
1306
+ ) {
1307
+ return undefined;
1308
+ }
1309
+ return {
1310
+ leftPx: roundNumber(
1311
+ scale.renderPageFrame.leftPx +
1312
+ (rect.xTwips - scale.physicalBoundsTwips.xTwips) * scale.pxPerTwipX,
1313
+ ),
1314
+ topPx: roundNumber(
1315
+ scale.renderPageFrame.topPx +
1316
+ (rect.yTwips - scale.physicalBoundsTwips.yTwips) * scale.pxPerTwipY,
1317
+ ),
1318
+ widthPx: roundNumber(Math.max(0, rect.widthTwips * scale.pxPerTwipX)),
1319
+ heightPx: roundNumber(Math.max(0, rect.heightTwips * scale.pxPerTwipY)),
1320
+ coordinateSpace: "frame-px",
1321
+ };
1322
+ }
1323
+
1324
+ function l04FrameTwipsPointToFramePx(
1325
+ point: { readonly x?: number; readonly y?: number } | undefined,
1326
+ scale: AdjacentGeometryFramePixelScale,
1327
+ ): AdjacentGeometryFramePxPoint | undefined {
1328
+ if (
1329
+ !point ||
1330
+ point.x === undefined ||
1331
+ point.y === undefined ||
1332
+ scale.physicalBoundsTwips.xTwips === undefined ||
1333
+ scale.physicalBoundsTwips.yTwips === undefined
1334
+ ) {
1335
+ return undefined;
1336
+ }
1337
+ return {
1338
+ xPx: roundNumber(
1339
+ scale.renderPageFrame.leftPx +
1340
+ (point.x - scale.physicalBoundsTwips.xTwips) * scale.pxPerTwipX,
1341
+ ),
1342
+ yPx: roundNumber(
1343
+ scale.renderPageFrame.topPx +
1344
+ (point.y - scale.physicalBoundsTwips.yTwips) * scale.pxPerTwipY,
1345
+ ),
1346
+ coordinateSpace: "frame-px",
1347
+ };
1348
+ }
1349
+
1350
+ function rangeRectFromPoints(
1351
+ start: AdjacentGeometryFramePxPoint,
1352
+ end: AdjacentGeometryFramePxPoint,
1353
+ ): AdjacentGeometryFramePxRect {
1354
+ const leftPx = Math.min(start.xPx, end.xPx);
1355
+ const topPx = Math.min(start.yPx, end.yPx);
1356
+ return {
1357
+ leftPx,
1358
+ topPx,
1359
+ widthPx: roundNumber(Math.abs(end.xPx - start.xPx)),
1360
+ heightPx: roundNumber(Math.abs(end.yPx - start.yPx)),
1361
+ coordinateSpace: "frame-px",
1362
+ };
1363
+ }
1364
+
1365
+ function classifyPageIndex(
1366
+ normalizedWordPageIndex: number | undefined,
1367
+ runtimePageIndex: number | undefined,
1368
+ ): AdjacentGeometryPageIndexStatus {
1369
+ if (normalizedWordPageIndex === undefined) return "word-page-missing";
1370
+ if (runtimePageIndex === undefined) return "runtime-missing";
1371
+ return normalizedWordPageIndex === runtimePageIndex
1372
+ ? "matches-runtime"
1373
+ : "differs-from-runtime";
1374
+ }
1375
+
1376
+ function numberingRuntimeReadbackKey(
1377
+ docId: string | undefined,
1378
+ numberingKeyValue: string | undefined,
1379
+ numberingInstanceId: string | undefined,
1380
+ level: number | undefined,
1381
+ ): string | undefined {
1382
+ return docId &&
1383
+ numberingKeyValue &&
1384
+ numberingInstanceId &&
1385
+ level !== undefined
1386
+ ? `${docId}\0${numberingKeyValue}\0${numberingInstanceId}\0${level}`
1387
+ : undefined;
1388
+ }
1389
+
618
1390
  function normalizeRects(raw: unknown): readonly AdjacentGeometryRect[] {
619
1391
  if (Array.isArray(raw)) return raw.map(normalizeRect).filter(isRect);
620
1392
  const maybeRect = normalizeRect(raw);
@@ -661,6 +1433,31 @@ function normalizeRect(raw: unknown): AdjacentGeometryRect | undefined {
661
1433
  };
662
1434
  }
663
1435
 
1436
+ function normalizeFramePxRect(
1437
+ raw: unknown,
1438
+ ): AdjacentGeometryFramePxRect | undefined {
1439
+ const rect = record(raw);
1440
+ const leftPx = numberValue(rect.leftPx);
1441
+ const topPx = numberValue(rect.topPx);
1442
+ const widthPx = numberValue(rect.widthPx);
1443
+ const heightPx = numberValue(rect.heightPx);
1444
+ if (
1445
+ leftPx === undefined ||
1446
+ topPx === undefined ||
1447
+ widthPx === undefined ||
1448
+ heightPx === undefined
1449
+ ) {
1450
+ return undefined;
1451
+ }
1452
+ return {
1453
+ leftPx: roundNumber(leftPx),
1454
+ topPx: roundNumber(topPx),
1455
+ widthPx: roundNumber(widthPx),
1456
+ heightPx: roundNumber(heightPx),
1457
+ coordinateSpace: "frame-px",
1458
+ };
1459
+ }
1460
+
664
1461
  function normalizeTwipsRect(raw: unknown):
665
1462
  | {
666
1463
  readonly xTwips?: number;
@@ -819,6 +1616,14 @@ function numberValue(value: unknown): number | undefined {
819
1616
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
820
1617
  }
821
1618
 
1619
+ function isFinitePositive(value: number): boolean {
1620
+ return Number.isFinite(value) && value > 0;
1621
+ }
1622
+
1623
+ function roundNumber(value: number): number {
1624
+ return Number.parseFloat(value.toFixed(6));
1625
+ }
1626
+
822
1627
  function nullableNumberValue(value: unknown): number | null | undefined {
823
1628
  return value === null ? null : numberValue(value);
824
1629
  }