@beyondwork/docx-react-component 1.0.110 → 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 (52) 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/runtime/geometry/adjacent-geometry-intake.ts +373 -5
  5. package/src/runtime/geometry/caret-geometry.ts +219 -7
  6. package/src/runtime/geometry/geometry-index.ts +35 -10
  7. package/src/runtime/geometry/object-handles.ts +42 -1
  8. package/src/runtime/layout/index.ts +3 -0
  9. package/src/runtime/layout/inert-layout-facet.ts +13 -0
  10. package/src/runtime/layout/layout-engine-instance.ts +2 -0
  11. package/src/runtime/layout/layout-engine-version.ts +32 -2
  12. package/src/runtime/layout/layout-facet-types.ts +3 -0
  13. package/src/runtime/layout/page-graph.ts +81 -7
  14. package/src/runtime/layout/project-block-fragments.ts +144 -1
  15. package/src/runtime/layout/public-facet.ts +160 -0
  16. package/src/runtime/scopes/adjacent-geometry-evidence.ts +456 -0
  17. package/src/runtime/scopes/compile-scope-bundle.ts +8 -0
  18. package/src/runtime/scopes/evidence.ts +16 -0
  19. package/src/runtime/scopes/index.ts +13 -0
  20. package/src/runtime/scopes/semantic-scope-types.ts +67 -0
  21. package/src/ui-tailwind/debug/layer11-consumer-readiness.ts +104 -0
  22. package/src/ui-tailwind/editor-surface/pm-page-break-decorations.ts +50 -5
  23. package/src/ui-tailwind/editor-surface/tw-prosemirror-surface.tsx +26 -0
  24. package/src/README.md +0 -85
  25. package/src/api/README.md +0 -26
  26. package/src/api/v3/README.md +0 -91
  27. package/src/component-inventory.md +0 -99
  28. package/src/core/README.md +0 -10
  29. package/src/core/commands/README.md +0 -3
  30. package/src/core/schema/README.md +0 -3
  31. package/src/core/selection/README.md +0 -3
  32. package/src/core/state/README.md +0 -3
  33. package/src/io/README.md +0 -10
  34. package/src/io/export/README.md +0 -3
  35. package/src/io/normalize/README.md +0 -3
  36. package/src/io/ooxml/README.md +0 -3
  37. package/src/io/opc/README.md +0 -3
  38. package/src/model/README.md +0 -3
  39. package/src/preservation/README.md +0 -3
  40. package/src/review/README.md +0 -16
  41. package/src/review/store/README.md +0 -3
  42. package/src/runtime/README.md +0 -3
  43. package/src/ui/README.md +0 -30
  44. package/src/ui/comments/README.md +0 -3
  45. package/src/ui/compatibility/README.md +0 -3
  46. package/src/ui/editor-surface/README.md +0 -3
  47. package/src/ui/review/README.md +0 -3
  48. package/src/ui/status/README.md +0 -3
  49. package/src/ui/theme/README.md +0 -3
  50. package/src/ui/toolbar/README.md +0 -3
  51. package/src/ui-tailwind/debug/README.md +0 -22
  52. package/src/validation/README.md +0 -3
@@ -34,9 +34,14 @@
34
34
  */
35
35
 
36
36
  import type { EditorStoryTarget } from "../../api/public-types";
37
+ import type {
38
+ PublicLineRunAnchor,
39
+ PublicTwipsRect,
40
+ } from "../layout/public-facet.ts";
37
41
  import type {
38
42
  RenderFrame,
39
43
  RenderFrameRect,
44
+ RenderLine,
40
45
  RenderStoryRegion,
41
46
  } from "../render/index.ts";
42
47
  import type {
@@ -58,6 +63,9 @@ export function resolveCaretGeometry(
58
63
  if (!frame) return null;
59
64
  if (!Number.isFinite(offset)) return null;
60
65
 
66
+ const projectedCaret = resolveProducerCaretGeometry(frame, offset, story);
67
+ if (projectedCaret) return projectedCaret;
68
+
61
69
  const anchorRect = frame.anchorIndex.byRuntimeOffset(offset, story);
62
70
  if (!anchorRect) return null;
63
71
 
@@ -113,12 +121,7 @@ export function resolveSelectionRects(
113
121
  if (lo === hi) {
114
122
  const caret = resolveCaretGeometry(frame, lo, range.story);
115
123
  if (!caret) return [];
116
- // Caret rect position for a collapsed range is an exact anchor-index
117
- // read. The placeholder-grade metadata on `CaretGeometry` (direction,
118
- // baseline) applies to the caret as a whole, not to the rect's
119
- // position, so make the rect precision explicit for downstream
120
- // compositor consumers.
121
- return [{ ...caret.rect, precision: "exact" }];
124
+ return [{ ...caret.rect, precision: caret.rect.precision ?? "exact" }];
122
125
  }
123
126
 
124
127
  const lineRects = resolveLineSelectionRects(frame, lo, hi, range.story);
@@ -178,7 +181,10 @@ function resolveLineSelectionRects(
178
181
  : blockFrom +
179
182
  Math.floor((span * (i + 1)) / Math.max(1, lineCount));
180
183
  if (lineTo <= from || lineFrom >= to) continue;
181
- rects.push(toGeometryRect(line.frame, "within-tolerance"));
184
+ rects.push(
185
+ resolveProducerSelectionRect(line, lineFrom, lineTo, from, to) ??
186
+ toGeometryRect(line.frame, "within-tolerance"),
187
+ );
182
188
  }
183
189
  }
184
190
  }
@@ -186,6 +192,212 @@ function resolveLineSelectionRects(
186
192
  return rects;
187
193
  }
188
194
 
195
+ function resolveProducerCaretGeometry(
196
+ frame: RenderFrame,
197
+ offset: number,
198
+ story?: EditorStoryTarget,
199
+ ): CaretGeometry | null {
200
+ const located = findProducerLineForOffset(frame, offset, story);
201
+ if (!located) return null;
202
+ const { line, lineFrom, lineTo } = located;
203
+ const lineRectTwips = producerLineRectTwips(line);
204
+ if (!lineRectTwips) return null;
205
+ const contentRectTwips = producerLineContentRectTwips(line) ?? lineRectTwips;
206
+ const direction = producerLineDirection(line);
207
+ const span = Math.max(1, lineTo - lineFrom);
208
+ const ratio = clamp((offset - lineFrom) / span, 0, 1);
209
+ const inlineRatio = direction === "rtl" ? 1 - ratio : ratio;
210
+ const caretXTwips =
211
+ contentRectTwips.xTwips + contentRectTwips.widthTwips * inlineRatio;
212
+ const caretFrame = projectLineTwipsRect(line, lineRectTwips, {
213
+ xTwips: caretXTwips,
214
+ yTwips: lineRectTwips.yTwips,
215
+ widthTwips: 0,
216
+ heightTwips: lineRectTwips.heightTwips,
217
+ });
218
+ if (!caretFrame) return null;
219
+ const baseline =
220
+ line.line.baselinePageYTwips !== undefined
221
+ ? projectPageYToLineOffsetPx(
222
+ line,
223
+ lineRectTwips,
224
+ line.line.baselinePageYTwips,
225
+ )
226
+ : producerRunBaselinePageYTwips(line) !== undefined
227
+ ? projectPageYToLineOffsetPx(
228
+ line,
229
+ lineRectTwips,
230
+ producerRunBaselinePageYTwips(line)!,
231
+ )
232
+ : resolveBaselinePx(caretFrame);
233
+ return {
234
+ rect: {
235
+ ...toGeometryRect(caretFrame, "within-tolerance"),
236
+ widthPx: 0,
237
+ },
238
+ baseline,
239
+ height: caretFrame.heightPx,
240
+ direction,
241
+ precision: "within-tolerance",
242
+ };
243
+ }
244
+
245
+ function findProducerLineForOffset(
246
+ frame: RenderFrame,
247
+ offset: number,
248
+ story?: EditorStoryTarget,
249
+ ):
250
+ | {
251
+ line: RenderLine;
252
+ lineFrom: number;
253
+ lineTo: number;
254
+ }
255
+ | null {
256
+ if (!Array.isArray(frame.pages)) return null;
257
+ for (const page of frame.pages) {
258
+ for (const region of collectRegions(page.regions)) {
259
+ if (!storyMatches(region.storyTarget, story)) continue;
260
+ for (const block of region.blocks) {
261
+ const blockFrom = block.fragment.from;
262
+ const blockTo = block.fragment.to;
263
+ if (offset < blockFrom || offset > blockTo) continue;
264
+ if (block.lines.length === 0) continue;
265
+ const span = Math.max(1, blockTo - blockFrom);
266
+ const lineCount = block.lines.length;
267
+ for (let i = 0; i < lineCount; i += 1) {
268
+ const lineFrom =
269
+ blockFrom + Math.floor((span * i) / Math.max(1, lineCount));
270
+ const lineTo =
271
+ i === lineCount - 1
272
+ ? blockTo
273
+ : blockFrom +
274
+ Math.floor((span * (i + 1)) / Math.max(1, lineCount));
275
+ const contains =
276
+ i === lineCount - 1
277
+ ? offset >= lineFrom && offset <= lineTo
278
+ : offset >= lineFrom && offset < lineTo;
279
+ if (!contains) continue;
280
+ const line = block.lines[i]!;
281
+ if (!producerLineRectTwips(line)) continue;
282
+ return { line, lineFrom, lineTo };
283
+ }
284
+ }
285
+ }
286
+ }
287
+ return null;
288
+ }
289
+
290
+ function resolveProducerSelectionRect(
291
+ line: RenderLine,
292
+ lineFrom: number,
293
+ lineTo: number,
294
+ from: number,
295
+ to: number,
296
+ ): GeometryRect | null {
297
+ const lineRectTwips = producerLineRectTwips(line);
298
+ if (!lineRectTwips) return null;
299
+ const contentRectTwips = producerLineContentRectTwips(line) ?? lineRectTwips;
300
+ const span = Math.max(1, lineTo - lineFrom);
301
+ const startRatio = clamp((Math.max(from, lineFrom) - lineFrom) / span, 0, 1);
302
+ const endRatio = clamp((Math.min(to, lineTo) - lineFrom) / span, 0, 1);
303
+ if (endRatio <= startRatio) return null;
304
+ const direction = producerLineDirection(line);
305
+ const leftRatio = direction === "rtl" ? 1 - endRatio : startRatio;
306
+ const rightRatio = direction === "rtl" ? 1 - startRatio : endRatio;
307
+ const leftTwips =
308
+ contentRectTwips.xTwips + contentRectTwips.widthTwips * leftRatio;
309
+ const rightTwips =
310
+ contentRectTwips.xTwips + contentRectTwips.widthTwips * rightRatio;
311
+ const frameRect = projectLineTwipsRect(line, lineRectTwips, {
312
+ xTwips: leftTwips,
313
+ yTwips: contentRectTwips.yTwips,
314
+ widthTwips: Math.max(0, rightTwips - leftTwips),
315
+ heightTwips: contentRectTwips.heightTwips,
316
+ });
317
+ return frameRect ? toGeometryRect(frameRect, "within-tolerance") : null;
318
+ }
319
+
320
+ function producerLineRectTwips(line: RenderLine): PublicTwipsRect | undefined {
321
+ return line.line.rectTwips ?? line.line.runAnchors?.[0]?.lineRectTwips;
322
+ }
323
+
324
+ function producerLineContentRectTwips(
325
+ line: RenderLine,
326
+ ): PublicTwipsRect | undefined {
327
+ const anchors = line.line.runAnchors ?? [];
328
+ if (anchors.length === 0) return undefined;
329
+ return unionRunRectTwips(anchors);
330
+ }
331
+
332
+ function producerLineDirection(line: RenderLine): "ltr" | "rtl" {
333
+ return line.line.direction ?? line.line.runAnchors?.[0]?.direction ?? "ltr";
334
+ }
335
+
336
+ function producerRunBaselinePageYTwips(
337
+ line: RenderLine,
338
+ ): number | undefined {
339
+ return line.line.runAnchors?.[0]?.baselinePageYTwips;
340
+ }
341
+
342
+ function unionRunRectTwips(
343
+ anchors: readonly PublicLineRunAnchor[],
344
+ ): PublicTwipsRect | undefined {
345
+ let left = Number.POSITIVE_INFINITY;
346
+ let top = Number.POSITIVE_INFINITY;
347
+ let right = Number.NEGATIVE_INFINITY;
348
+ let bottom = Number.NEGATIVE_INFINITY;
349
+ for (const anchor of anchors) {
350
+ const rect = anchor.runRectTwips;
351
+ left = Math.min(left, rect.xTwips);
352
+ top = Math.min(top, rect.yTwips);
353
+ right = Math.max(right, rect.xTwips + rect.widthTwips);
354
+ bottom = Math.max(bottom, rect.yTwips + rect.heightTwips);
355
+ }
356
+ if (!Number.isFinite(left) || !Number.isFinite(top)) return undefined;
357
+ return {
358
+ xTwips: left,
359
+ yTwips: top,
360
+ widthTwips: Math.max(0, right - left),
361
+ heightTwips: Math.max(0, bottom - top),
362
+ };
363
+ }
364
+
365
+ function projectLineTwipsRect(
366
+ line: RenderLine,
367
+ lineRectTwips: PublicTwipsRect,
368
+ rect: PublicTwipsRect,
369
+ ): RenderFrameRect | null {
370
+ if (lineRectTwips.widthTwips <= 0 || lineRectTwips.heightTwips <= 0) {
371
+ return null;
372
+ }
373
+ const scaleX = line.frame.widthPx / lineRectTwips.widthTwips;
374
+ const scaleY = line.frame.heightPx / lineRectTwips.heightTwips;
375
+ return {
376
+ leftPx: line.frame.leftPx + (rect.xTwips - lineRectTwips.xTwips) * scaleX,
377
+ topPx: line.frame.topPx + (rect.yTwips - lineRectTwips.yTwips) * scaleY,
378
+ widthPx: rect.widthTwips * scaleX,
379
+ heightPx: rect.heightTwips * scaleY,
380
+ };
381
+ }
382
+
383
+ function projectPageYToLineOffsetPx(
384
+ line: RenderLine,
385
+ lineRectTwips: PublicTwipsRect,
386
+ pageYTwips: number,
387
+ ): number {
388
+ if (lineRectTwips.heightTwips <= 0) return resolveBaselinePx(line.frame);
389
+ const scaleY = line.frame.heightPx / lineRectTwips.heightTwips;
390
+ return clamp(
391
+ (pageYTwips - lineRectTwips.yTwips) * scaleY,
392
+ 0,
393
+ line.frame.heightPx,
394
+ );
395
+ }
396
+
397
+ function clamp(value: number, min: number, max: number): number {
398
+ return Math.min(Math.max(value, min), max);
399
+ }
400
+
189
401
  function collectRegions(
190
402
  regions: RenderFrame["pages"][number]["regions"],
191
403
  ): readonly RenderStoryRegion[] {
@@ -27,6 +27,7 @@ import {
27
27
  type CanonicalTableCellLayoutInput,
28
28
  type CanonicalTableRowLayoutInput,
29
29
  } from "../../model/canonical-layout-inputs.ts";
30
+ import type { PublicTwipsRect } from "../layout/public-facet.ts";
30
31
  import type {
31
32
  RenderFrame,
32
33
  RenderFrameRect,
@@ -1309,25 +1310,32 @@ function appendPageLocalObjectHandleEntries(input: {
1309
1310
  story.kind === "header" ? page.regions.header?.frame : page.regions.footer?.frame;
1310
1311
  if (!regionFrame) continue;
1311
1312
  for (const object of story.anchoredObjects) {
1312
- const objectFrame = pageLocalObjectFrame(
1313
- regionFrame,
1314
- object.extentTwips,
1315
- pxPerTwip,
1313
+ const objectFrame = object.anchorRectTwips
1314
+ ? pageLocalObjectAnchorFrame(page, object.anchorRectTwips)
1315
+ : pageLocalObjectFrame(regionFrame, object.extentTwips, pxPerTwip);
1316
+ const entryPrecision: GeometryPrecision = object.anchorRectTwips
1317
+ ? "within-tolerance"
1318
+ : "heuristic";
1319
+ const entryStatus: GeometryRehydrationStatus = object.anchorRectTwips
1320
+ ? "realized"
1321
+ : "requires-rehydration";
1322
+ const handleRects = buildObjectHandleRectsFromRect(
1323
+ objectFrame,
1324
+ entryPrecision,
1316
1325
  );
1317
- const handleRects = buildObjectHandleRectsFromRect(objectFrame, "heuristic");
1318
1326
  const sourceIdentity: GeometrySourceIdentity = {
1319
1327
  storyKey: story.storyKey,
1320
1328
  objectKey: object.objectId,
1321
1329
  objectKind: object.sourceType,
1322
1330
  editPosture: object.preserveOnly ? "preserve-only" : "editable",
1323
- joinKind: "block-scoped",
1331
+ joinKind: object.anchorRectTwips ? "direct" : "block-scoped",
1324
1332
  };
1325
1333
  const existing = entries.get(object.objectId);
1326
1334
  if (existing) {
1327
1335
  appendUnique(existing.pageIds, page.page.pageId);
1328
1336
  existing.rects.push(...handleRects);
1329
1337
  appendDivergenceIds(existing, divergenceIdsByObjectId.get(object.objectId));
1330
- if (existing.precision !== "heuristic") {
1338
+ if (existing.precision !== "heuristic" && entryPrecision === "heuristic") {
1331
1339
  existing.precision = "heuristic";
1332
1340
  existing.status = "requires-rehydration";
1333
1341
  existing.sourceIdentity = sourceIdentity;
@@ -1338,16 +1346,33 @@ function appendPageLocalObjectHandleEntries(input: {
1338
1346
  objectId: object.objectId,
1339
1347
  pageIds: [page.page.pageId],
1340
1348
  rects: [...handleRects],
1341
- status: "requires-rehydration",
1342
- precision: "heuristic",
1349
+ status: entryStatus,
1350
+ precision: entryPrecision,
1343
1351
  ...mutableLayoutDivergenceIds(divergenceIdsByObjectId.get(object.objectId)),
1344
1352
  sourceIdentity,
1345
1353
  });
1346
- recordPrecision(precision, "heuristic");
1354
+ recordPrecision(precision, entryPrecision);
1347
1355
  }
1348
1356
  }
1349
1357
  }
1350
1358
 
1359
+ function pageLocalObjectAnchorFrame(
1360
+ page: RenderPage,
1361
+ rect: PublicTwipsRect,
1362
+ ): RenderFrameRect {
1363
+ const pageWidthTwips = page.page.layout.pageWidth;
1364
+ const pageHeightTwips = page.page.layout.pageHeight;
1365
+ const scaleX = pageWidthTwips > 0 ? page.frame.widthPx / pageWidthTwips : 1;
1366
+ const scaleY =
1367
+ pageHeightTwips > 0 ? page.frame.heightPx / pageHeightTwips : scaleX;
1368
+ return {
1369
+ leftPx: page.frame.leftPx + rect.xTwips * scaleX,
1370
+ topPx: page.frame.topPx + rect.yTwips * scaleY,
1371
+ widthPx: rect.widthTwips * scaleX,
1372
+ heightPx: rect.heightTwips * scaleY,
1373
+ };
1374
+ }
1375
+
1351
1376
  function pageLocalObjectFrame(
1352
1377
  regionFrame: RenderFrameRect,
1353
1378
  extentTwips:
@@ -26,7 +26,12 @@
26
26
  * index 8 — rotate anchor (20 px above the top-center handle)
27
27
  */
28
28
 
29
- import type { RenderFrame, RenderFrameRect } from "../render/index.ts";
29
+ import type { PublicTwipsRect } from "../layout/public-facet.ts";
30
+ import type {
31
+ RenderFrame,
32
+ RenderFrameRect,
33
+ RenderPage,
34
+ } from "../render/index.ts";
30
35
  import type { GeometryPrecision, GeometryRect } from "./geometry-types.ts";
31
36
 
32
37
  const ROTATE_HANDLE_OFFSET_PX = 20;
@@ -36,6 +41,10 @@ export function resolveObjectHandles(
36
41
  objectId: string,
37
42
  ): readonly GeometryRect[] {
38
43
  if (!frame) return [];
44
+ const pageLocalBBox = resolvePageLocalObjectFrame(frame, objectId);
45
+ if (pageLocalBBox) {
46
+ return buildObjectHandleRectsFromRect(pageLocalBBox, "within-tolerance");
47
+ }
39
48
  const bbox = frame.anchorIndex.byObjectId(objectId);
40
49
  if (!bbox) return [];
41
50
  return buildObjectHandleRectsFromRect(bbox);
@@ -78,3 +87,35 @@ export function buildObjectHandleRectsFromRect(
78
87
  point(midX, topPx - ROTATE_HANDLE_OFFSET_PX), // 8 rotate
79
88
  ];
80
89
  }
90
+
91
+ function resolvePageLocalObjectFrame(
92
+ frame: RenderFrame,
93
+ objectId: string,
94
+ ): RenderFrameRect | null {
95
+ if (!Array.isArray(frame.pages)) return null;
96
+ for (const page of frame.pages) {
97
+ for (const story of page.page.frame?.pageLocalStories ?? []) {
98
+ for (const object of story.anchoredObjects) {
99
+ if (object.objectId !== objectId || !object.anchorRectTwips) continue;
100
+ return projectPageTwipsRect(page, object.anchorRectTwips);
101
+ }
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+
107
+ function projectPageTwipsRect(
108
+ page: RenderPage,
109
+ rect: PublicTwipsRect,
110
+ ): RenderFrameRect {
111
+ const pageWidthTwips = page.page.layout.pageWidth;
112
+ const pageHeightTwips = page.page.layout.pageHeight;
113
+ const scaleX = pageWidthTwips > 0 ? page.frame.widthPx / pageWidthTwips : 1;
114
+ const scaleY = pageHeightTwips > 0 ? page.frame.heightPx / pageHeightTwips : scaleX;
115
+ return {
116
+ leftPx: page.frame.leftPx + rect.xTwips * scaleX,
117
+ topPx: page.frame.topPx + rect.yTwips * scaleY,
118
+ widthPx: rect.widthTwips * scaleX,
119
+ heightPx: rect.heightTwips * scaleY,
120
+ };
121
+ }
@@ -181,11 +181,14 @@ export {
181
181
  type PublicRegionKind,
182
182
  type PublicBlockFragment,
183
183
  type PublicLineBox,
184
+ type PublicLineRunAnchor,
184
185
  type PublicNoteAllocation,
185
186
  type PublicPageAnchor,
186
187
  type PublicPageFrame,
187
188
  type PublicPageLocalStoryInstance,
189
+ type PublicPagePaginationTelemetry,
188
190
  type PublicPageSpan,
191
+ type PublicPaginationTelemetry,
189
192
  type PublicSectionNode,
190
193
  type PublicResolvedPageStories,
191
194
  type PublicResolvedStoryField,
@@ -10,6 +10,7 @@ import type {
10
10
  LayoutFacetEvent,
11
11
  PublicFieldDirtinessReport,
12
12
  PublicMeasurementFidelity,
13
+ PublicPaginationTelemetry,
13
14
  WordReviewEditorLayoutFacet,
14
15
  } from "./public-facet.ts";
15
16
  import { MARGIN_PRESET_CATALOG } from "./margin-preset-catalog.ts";
@@ -20,6 +21,17 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
20
21
  families: [],
21
22
  revision: 0,
22
23
  };
24
+ const emptyPaginationTelemetry: PublicPaginationTelemetry = {
25
+ revision: 0,
26
+ pageCount: 0,
27
+ materializedPageCount: 0,
28
+ unpaginatedPageCount: 0,
29
+ bodyFragmentCount: 0,
30
+ bodyBlockReferenceCount: 0,
31
+ averageBodyFragmentsPerMaterializedPage: 0,
32
+ averageBodyBlockReferencesPerMaterializedPage: 0,
33
+ pages: [],
34
+ };
23
35
  const fidelity: PublicMeasurementFidelity = "empirical";
24
36
  return {
25
37
  getPageCount: () => 0,
@@ -39,6 +51,7 @@ export function createInertLayoutFacet(): WordReviewEditorLayoutFacet {
39
51
  getStoryBlocksForRegion: () => [],
40
52
  getDocumentEndnoteBlocks: () => [],
41
53
  getFragmentsForPage: () => [],
54
+ getPaginationTelemetry: () => emptyPaginationTelemetry,
42
55
  getPageFormatCatalog: () => PAGE_FORMAT_CATALOG,
43
56
  getActivePageFormat: () => null,
44
57
  getMarginPresetCatalog: () => MARGIN_PRESET_CATALOG,
@@ -693,6 +693,7 @@ export function createLayoutEngine(
693
693
  pages,
694
694
  bodyFragmentsByPageIndex,
695
695
  pageStack.fragmentMeasurementsByPageIndex,
696
+ mainSurface,
696
697
  );
697
698
  // P8.1b — merge per-note fragments (regionKind: "footnote-area") into the
698
699
  // main fragments map so buildPageGraph sees them alongside body fragments.
@@ -881,6 +882,7 @@ export function createLayoutEngine(
881
882
  freshSnapshotsToRebuild,
882
883
  freshBodyFragmentsByPageIndex,
883
884
  freshResult.fragmentMeasurementsByPageIndex,
885
+ mainSurface,
884
886
  );
885
887
  // P8.1b — merge per-note fragments into the fresh fragments map.
886
888
  const freshFragmentsByPageIndex = new Map(freshBodyFragmentsByPageIndex);
@@ -1248,8 +1248,34 @@
1248
1248
  * to v88. Cache envelopes from v87 invalidate because page graph
1249
1249
  * materialization state, cache-key semantics, geometry projections, and
1250
1250
  * surface preview shapes can change.
1251
+ *
1252
+ * 89 — Ship-side rollup: pe2 commit covering L04 pagination telemetry read
1253
+ * model — `WordReviewEditorLayoutFacet` now exposes
1254
+ * `getPaginationTelemetry()`, with per-page materialization, body-fragment
1255
+ * counts, body block-reference counts, line-box counts, and
1256
+ * materialized-page averages. Pagination output and cached graph payloads
1257
+ * are unchanged; cache envelopes from v88 invalidate because the public
1258
+ * layout facet interface grew a method used by viewport-window consumers.
1259
+ * The pe2 history-block bumped to v83 in isolation; ship-preview's rollup
1260
+ * advances to v89.
1261
+ *
1262
+ * 90 — Ship-side rollup: pe2 commit 2fcc15a10 ("feat(04): publish precision
1263
+ * producer layout facts") landed L04 precision producer facts — runtime
1264
+ * line boxes now carry page-local rects, baseline page coordinates,
1265
+ * paragraph direction, and per-run layout-estimate anchors with first/last
1266
+ * glyph bounds. Page-local anchored-object ledgers can carry
1267
+ * `anchorRectTwips` when canonical anchor metadata gives L04 a source/frame
1268
+ * placement. Pe2 commits a22c2e4ab ("feat(05): consume l04 producer
1269
+ * geometry facts") + 6e71ee2ad ("feat(05): publish frame-pixel adjacent
1270
+ * geometry") additionally brushed `src/runtime/geometry/**`, and pe2
1271
+ * commits 883e46bb7 + 8519112d0 ("fix(11): suppress table-internal page
1272
+ * chrome / anchors") brushed `src/runtime/layout/page-graph.ts` without
1273
+ * separate pe2 bumps. The pe2 history-block bumped to v84 in isolation;
1274
+ * ship-preview's rollup advances to v90. Pagination output is unchanged,
1275
+ * but public graph/read-model semantics grew for L05 exact caret/object
1276
+ * projection consumers and for L11 page-anchor suppression in tables.
1251
1277
  */
1252
- export const LAYOUT_ENGINE_VERSION = 88 as const;
1278
+ export const LAYOUT_ENGINE_VERSION = 90 as const;
1253
1279
 
1254
1280
  /**
1255
1281
  * Serialization schema version for the LayCache envelope and cached payload
@@ -1290,5 +1316,9 @@ export const LAYOUT_ENGINE_VERSION = 88 as const;
1290
1316
  * `numberingRows[]` for all numbered paragraphs represented by the
1291
1317
  * fragment, including table/SDT-nested paragraphs. Pagination output is
1292
1318
  * unchanged, but cached graph payload shape grows.
1319
+ * 7 — L04 precision producer graph payload: line boxes carry page-local
1320
+ * rect/baseline/direction/run-anchor facts, and page-local anchored
1321
+ * object ledgers can carry source-anchor `anchorRectTwips`. Pagination
1322
+ * output is unchanged, but cached graph payload shape grows.
1293
1323
  */
1294
- export const LAYCACHE_SCHEMA_VERSION = 6 as const;
1324
+ export const LAYCACHE_SCHEMA_VERSION = 7 as const;
@@ -42,11 +42,14 @@ export type {
42
42
  PublicRegionKind,
43
43
  PublicBlockFragment,
44
44
  PublicLineBox,
45
+ PublicLineRunAnchor,
45
46
  PublicNoteAllocation,
46
47
  PublicPageAnchor,
47
48
  PublicPageFrame,
48
49
  PublicPageLocalStoryInstance,
50
+ PublicPagePaginationTelemetry,
49
51
  PublicPageSpan,
52
+ PublicPaginationTelemetry,
50
53
  PublicSectionNode,
51
54
  PublicResolvedPageStories,
52
55
  PublicResolvedStoryField,
@@ -77,6 +77,7 @@ export type {
77
77
  RuntimeFragmentLayoutObject,
78
78
  RuntimeBlockFragment,
79
79
  RuntimeLineBox,
80
+ RuntimeLineRunAnchor,
80
81
  RuntimeNoteAllocation,
81
82
  RuntimePageAnchor,
82
83
  } from "../../model/layout/page-graph-types.ts";
@@ -566,6 +567,7 @@ function buildPageLocalStoryInstance(
566
567
  kind: target.kind,
567
568
  variant: target.variant,
568
569
  relationshipId: target.relationshipId,
570
+ region,
569
571
  })
570
572
  : { objects: [], divergences: [] };
571
573
  const signature = buildPageLocalStorySignature({
@@ -621,6 +623,10 @@ function buildPageLocalStorySignature(input: {
621
623
  object.display,
622
624
  object.extentTwips?.widthTwips ?? "",
623
625
  object.extentTwips?.heightTwips ?? "",
626
+ object.anchorRectTwips?.xTwips ?? "",
627
+ object.anchorRectTwips?.yTwips ?? "",
628
+ object.anchorRectTwips?.widthTwips ?? "",
629
+ object.anchorRectTwips?.heightTwips ?? "",
624
630
  object.relationshipIds?.join(",") ?? "",
625
631
  object.mediaIds?.join(",") ?? "",
626
632
  object.wrapMode ?? "",
@@ -732,6 +738,7 @@ interface StoryObjectContext {
732
738
  kind: "header" | "footer";
733
739
  variant: RuntimePageLocalStoryInstance["variant"];
734
740
  relationshipId: string;
741
+ region?: RuntimePageRegion;
735
742
  }
736
743
 
737
744
  function collectStoryAnchoredObjects(
@@ -836,13 +843,20 @@ function collectStoryAnchoredObjects(
836
843
  const preserveHint = getDrawingFramePreserveHint(inline);
837
844
  const relationshipIds = collectDrawingRelationshipIds(inline);
838
845
  const display = inline.anchor.display;
846
+ const extentTwips = extentTwipsFromEmu(
847
+ inline.anchor.extent.widthEmu,
848
+ inline.anchor.extent.heightEmu,
849
+ );
839
850
  pushObject({
840
851
  objectId: getDrawingFrameObjectId(inline, context.storyKey, ordinal),
841
852
  sourceType: "drawing-frame",
842
853
  display,
843
- extentTwips: extentTwipsFromEmu(
844
- inline.anchor.extent.widthEmu,
845
- inline.anchor.extent.heightEmu,
854
+ extentTwips,
855
+ anchorRectTwips: resolveObjectAnchorRectTwips(
856
+ context.region,
857
+ extentTwips,
858
+ inline.anchor.positionH,
859
+ inline.anchor.positionV,
846
860
  ),
847
861
  ...(relationshipIds.length > 0 ? { relationshipIds } : {}),
848
862
  ...(inline.content.type === "picture" && inline.content.mediaId
@@ -863,15 +877,22 @@ function collectStoryAnchoredObjects(
863
877
  case "wordart":
864
878
  case "vml_shape": {
865
879
  const preserveHint = inline.preserveOnlyObject;
880
+ const extentTwips = preserveHint?.extentEmu
881
+ ? extentTwipsFromEmu(
882
+ preserveHint.extentEmu.widthEmu,
883
+ preserveHint.extentEmu.heightEmu,
884
+ )
885
+ : undefined;
866
886
  pushObject({
867
887
  objectId: getPreserveOnlyObjectId(inline, context.storyKey, ordinal),
868
888
  sourceType: sourceTypeForInlineObject(inline.type),
869
889
  display: preserveHint?.display ?? "unknown",
870
- ...(preserveHint?.extentEmu
890
+ ...(extentTwips
871
891
  ? {
872
- extentTwips: extentTwipsFromEmu(
873
- preserveHint.extentEmu.widthEmu,
874
- preserveHint.extentEmu.heightEmu,
892
+ extentTwips,
893
+ anchorRectTwips: resolveObjectAnchorRectTwips(
894
+ context.region,
895
+ extentTwips,
875
896
  ),
876
897
  }
877
898
  : {}),
@@ -986,6 +1007,59 @@ function extentTwipsFromEmu(
986
1007
  };
987
1008
  }
988
1009
 
1010
+ function resolveObjectAnchorRectTwips(
1011
+ region: RuntimePageRegion | undefined,
1012
+ extentTwips: RuntimeStoryAnchoredObject["extentTwips"],
1013
+ positionH?: { relativeFrom: string; align?: string; offset?: number },
1014
+ positionV?: { relativeFrom: string; align?: string; offset?: number },
1015
+ ): RuntimeTwipsRect | undefined {
1016
+ if (!region?.rectTwips || !extentTwips) return undefined;
1017
+ const regionRect = region.rectTwips;
1018
+ const widthTwips = Math.max(0, extentTwips.widthTwips);
1019
+ const heightTwips = Math.max(0, extentTwips.heightTwips);
1020
+ return rect(
1021
+ resolveAxisTwips(
1022
+ regionRect.xTwips,
1023
+ regionRect.widthTwips,
1024
+ widthTwips,
1025
+ positionH?.align,
1026
+ positionH?.offset,
1027
+ ),
1028
+ resolveAxisTwips(
1029
+ regionRect.yTwips,
1030
+ regionRect.heightTwips,
1031
+ heightTwips,
1032
+ positionV?.align,
1033
+ positionV?.offset,
1034
+ ),
1035
+ widthTwips,
1036
+ heightTwips,
1037
+ );
1038
+ }
1039
+
1040
+ function resolveAxisTwips(
1041
+ originTwips: number,
1042
+ spanTwips: number,
1043
+ objectSpanTwips: number,
1044
+ align?: string,
1045
+ offsetEmu?: number,
1046
+ ): number {
1047
+ if (offsetEmu !== undefined) {
1048
+ return originTwips + Math.round(offsetEmu / EMUS_PER_TWIP);
1049
+ }
1050
+ switch (align) {
1051
+ case "center":
1052
+ return originTwips + Math.round((spanTwips - objectSpanTwips) / 2);
1053
+ case "right":
1054
+ case "bottom":
1055
+ return originTwips + Math.max(0, spanTwips - objectSpanTwips);
1056
+ case "left":
1057
+ case "top":
1058
+ default:
1059
+ return originTwips;
1060
+ }
1061
+ }
1062
+
989
1063
  function resolvePageInstanceFieldDisplayText(
990
1064
  family: string,
991
1065
  cachedDisplayText: string,