@beyondwork/docx-react-component 1.0.103 → 1.0.104

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 (33) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +63 -1
  3. package/src/api/v3/_runtime-handle.ts +2 -0
  4. package/src/api/v3/ai/outline.ts +2 -7
  5. package/src/api/v3/runtime/geometry.ts +79 -0
  6. package/src/io/ooxml/parse-drawing.ts +99 -1
  7. package/src/io/ooxml/parse-fields.ts +27 -6
  8. package/src/io/ooxml/parse-shapes.ts +130 -0
  9. package/src/model/canonical-document.ts +34 -3
  10. package/src/model/canonical-layout-inputs.ts +979 -0
  11. package/src/model/layout/index.ts +6 -0
  12. package/src/model/layout/page-graph-types.ts +61 -0
  13. package/src/model/layout/runtime-page-graph-types.ts +10 -0
  14. package/src/runtime/collab/runtime-collab-sync.ts +3 -3
  15. package/src/runtime/debug/build-debug-inspector-snapshot.ts +17 -4
  16. package/src/runtime/document-runtime.ts +30 -14
  17. package/src/runtime/event-refresh-hints.ts +3 -0
  18. package/src/runtime/formatting/formatting-context.ts +110 -9
  19. package/src/runtime/formatting/index.ts +2 -0
  20. package/src/runtime/formatting/layout-inputs.ts +67 -3
  21. package/src/runtime/geometry/caret-geometry.ts +82 -10
  22. package/src/runtime/geometry/geometry-facet.ts +36 -0
  23. package/src/runtime/geometry/geometry-index.ts +891 -0
  24. package/src/runtime/geometry/geometry-types.ts +221 -1
  25. package/src/runtime/geometry/index.ts +26 -0
  26. package/src/runtime/geometry/inert-geometry-facet.ts +3 -0
  27. package/src/runtime/geometry/replacement-envelope.ts +41 -2
  28. package/src/runtime/layout/layout-engine-version.ts +16 -1
  29. package/src/runtime/layout/page-graph.ts +191 -1
  30. package/src/runtime/prerender/graph-canonicalize.ts +30 -0
  31. package/src/runtime/surface-projection.ts +43 -3
  32. package/src/runtime/workflow/coordinator.ts +57 -11
  33. package/src/ui-tailwind/chrome-overlay/tw-page-stack-overlay-layer.tsx +3 -0
@@ -24,7 +24,10 @@
24
24
  */
25
25
 
26
26
  import type { EditorStoryTarget } from "../../api/public-types";
27
- import type { PublicPageRegion } from "../layout/public-facet.ts";
27
+ import type {
28
+ PublicMeasurementFidelity,
29
+ PublicPageRegion,
30
+ } from "../layout/public-facet.ts";
28
31
 
29
32
  // ---------------------------------------------------------------------------
30
33
  // G5 · Coordinate space tags
@@ -75,6 +78,11 @@ export type GeometrySpace = "twips" | "frame" | "overlay";
75
78
  */
76
79
  export type GeometryPrecision = "exact" | "within-tolerance" | "heuristic";
77
80
 
81
+ export type GeometryRehydrationStatus =
82
+ | "realized"
83
+ | "requires-rehydration"
84
+ | "unavailable";
85
+
78
86
  // ---------------------------------------------------------------------------
79
87
  // Core rect shape
80
88
  // ---------------------------------------------------------------------------
@@ -98,6 +106,200 @@ export interface GeometryRect {
98
106
  precision?: GeometryPrecision;
99
107
  }
100
108
 
109
+ // ---------------------------------------------------------------------------
110
+ // PE2 geometry index (Slice 1 · page-slice projection substrate)
111
+ // ---------------------------------------------------------------------------
112
+
113
+ export interface GeometryPrecisionCounts {
114
+ exact: number;
115
+ "within-tolerance": number;
116
+ heuristic: number;
117
+ }
118
+
119
+ export interface GeometryIndexCoverage {
120
+ status: GeometryRehydrationStatus;
121
+ pageCount: number;
122
+ regionCount: number;
123
+ sliceCount: number;
124
+ lineCount: number;
125
+ anchorCount: number;
126
+ hitTargetCount: number;
127
+ semanticEntryCount: number;
128
+ replacementEnvelopeCount: number;
129
+ objectHandleCount: number;
130
+ precision: GeometryPrecisionCounts;
131
+ }
132
+
133
+ export type GeometrySourceIdentityJoinKind = "direct" | "block-scoped";
134
+
135
+ /**
136
+ * Stable canonical identity joined onto projected geometry when the current
137
+ * substrate can prove the correspondence without consulting renderer state.
138
+ *
139
+ * `joinKind: "direct"` means the geometry node maps to the named canonical
140
+ * table/row/cell. `joinKind: "block-scoped"` means the geometry rect still
141
+ * belongs to the enclosing rendered block/line; object fields identify the
142
+ * canonical object carried by that block, not a true object bbox.
143
+ */
144
+ export interface GeometrySourceIdentity {
145
+ storyKey: string;
146
+ blockPath?: string;
147
+ tableKey?: string;
148
+ rowKey?: string;
149
+ cellKey?: string;
150
+ objectKey?: string;
151
+ inlinePath?: string;
152
+ objectKind?: string;
153
+ editPosture?: string;
154
+ joinKind: GeometrySourceIdentityJoinKind;
155
+ }
156
+
157
+ export interface GeometryIndex {
158
+ /**
159
+ * Source adapter for this index. PE2's target input is L04 page slices; the
160
+ * current substrate projects the render frame because it is the runtime's
161
+ * already materialized page-slice view.
162
+ */
163
+ source: "render-frame-adapter";
164
+ revision: number;
165
+ measurementFidelity: PublicMeasurementFidelity;
166
+ activeStory: EditorStoryTarget;
167
+ pages: readonly GeometryIndexPage[];
168
+ regions: readonly GeometryIndexRegion[];
169
+ slices: readonly GeometryIndexSlice[];
170
+ lines: readonly GeometryIndexLine[];
171
+ anchors: readonly AnchorGeometry[];
172
+ hitTargets: readonly GeometryHitTarget[];
173
+ semanticEntries: readonly SemanticDisplayEntry[];
174
+ replacementEnvelopes: readonly GeometryReplacementEnvelopeEntry[];
175
+ objectHandles: readonly GeometryObjectHandleEntry[];
176
+ coverage: GeometryIndexCoverage;
177
+ }
178
+
179
+ export interface GeometryIndexPage {
180
+ pageId: string;
181
+ pageIndex: number;
182
+ rect: GeometryRect;
183
+ regionIds: readonly string[];
184
+ }
185
+
186
+ export interface GeometryIndexRegion {
187
+ regionId: string;
188
+ pageId: string;
189
+ pageIndex: number;
190
+ kind: PublicPageRegion["kind"];
191
+ ordinal: number;
192
+ rect: GeometryRect;
193
+ sliceIds: readonly string[];
194
+ }
195
+
196
+ export interface GeometryIndexSlice {
197
+ sliceId: string;
198
+ pageId: string;
199
+ pageIndex: number;
200
+ regionId: string;
201
+ regionKind: PublicPageRegion["kind"];
202
+ blockId: string;
203
+ fragmentId: string;
204
+ kind: "paragraph" | "table" | "opaque" | "image-float" | "synthetic";
205
+ rect: GeometryRect;
206
+ lineIds: readonly string[];
207
+ sourceIdentity?: GeometrySourceIdentity;
208
+ }
209
+
210
+ export interface GeometryIndexLine {
211
+ lineId: string;
212
+ pageId: string;
213
+ pageIndex: number;
214
+ regionId: string;
215
+ regionKind: PublicPageRegion["kind"];
216
+ blockId: string;
217
+ fragmentId: string;
218
+ lineIndex: number;
219
+ rect: GeometryRect;
220
+ /** Baseline offset from the line rect's top, in frame pixels. */
221
+ baseline?: number;
222
+ baselinePrecision?: GeometryPrecision;
223
+ anchorIds: readonly string[];
224
+ }
225
+
226
+ export interface AnchorGeometry {
227
+ anchorId: string;
228
+ pageId: string;
229
+ pageIndex: number;
230
+ regionId: string;
231
+ regionKind: PublicPageRegion["kind"];
232
+ blockId?: string;
233
+ fragmentId?: string;
234
+ lineId?: string;
235
+ runtimeOffset?: number;
236
+ rect: GeometryRect;
237
+ precision: GeometryPrecision;
238
+ sourceIdentity?: GeometrySourceIdentity;
239
+ }
240
+
241
+ export interface GeometryHitTarget {
242
+ targetId: string;
243
+ pageId: string;
244
+ pageIndex: number;
245
+ regionId: string;
246
+ regionKind: PublicPageRegion["kind"];
247
+ blockId: string;
248
+ fragmentId: string;
249
+ lineIndex: number;
250
+ rect: GeometryRect;
251
+ precision: GeometryPrecision;
252
+ }
253
+
254
+ export type SemanticDisplayEntryKind =
255
+ | "text-line"
256
+ | "numbering-marker"
257
+ | "table-frame"
258
+ | "table-row"
259
+ | "table-cell"
260
+ | "toc-leader"
261
+ | "toc-page-label"
262
+ | "field-result"
263
+ | "inline-object"
264
+ | "floating-object"
265
+ | "placeholder"
266
+ | "background"
267
+ | "border"
268
+ | "debug-classification";
269
+
270
+ export interface SemanticDisplayEntry {
271
+ entryId: string;
272
+ kind: SemanticDisplayEntryKind;
273
+ pageId: string;
274
+ pageIndex: number;
275
+ regionId?: string;
276
+ regionKind?: PublicPageRegion["kind"];
277
+ sliceId?: string;
278
+ lineId?: string;
279
+ blockId?: string;
280
+ fragmentId?: string;
281
+ rowIndex?: number;
282
+ columnIndex?: number;
283
+ rect: GeometryRect;
284
+ status: GeometryRehydrationStatus;
285
+ precision: GeometryPrecision;
286
+ sourceIdentity?: GeometrySourceIdentity;
287
+ }
288
+
289
+ export interface GeometryReplacementEnvelopeEntry {
290
+ scopeId: string;
291
+ rects: readonly GeometryRect[];
292
+ status: GeometryRehydrationStatus;
293
+ precision: GeometryPrecision;
294
+ }
295
+
296
+ export interface GeometryObjectHandleEntry {
297
+ objectId: string;
298
+ rects: readonly GeometryRect[];
299
+ status: GeometryRehydrationStatus;
300
+ precision: GeometryPrecision;
301
+ }
302
+
101
303
  // ---------------------------------------------------------------------------
102
304
  // Per-granularity geometry nodes (read models; Slice 2+ populates them)
103
305
  // ---------------------------------------------------------------------------
@@ -200,6 +402,14 @@ export interface AnchorQuery {
200
402
 
201
403
  export type EnvelopeConfidence = "exact" | "medium" | "detached";
202
404
 
405
+ export interface EnvelopeContinuationState {
406
+ pageIds: readonly string[];
407
+ pageCount: number;
408
+ crossesPageBoundary: boolean;
409
+ continuedFromPreviousPage: boolean;
410
+ continuesToNextPage: boolean;
411
+ }
412
+
203
413
  export interface EnvelopeBundle {
204
414
  /**
205
415
  * Rect coverage for the resolved scope. Slice-5 substrate produces
@@ -211,6 +421,10 @@ export interface EnvelopeBundle {
211
421
  * walk if the count is lower than expected.
212
422
  */
213
423
  scopeRects: readonly GeometryRect[];
424
+ /** Page ids whose projected page frames intersect `scopeRects`. */
425
+ pageIds: readonly string[];
426
+ /** Continuation metadata for multi-page / partial-page replacement scopes. */
427
+ continuation: EnvelopeContinuationState;
214
428
  /** Anchor point consumers attach chrome to (top-left of first rect). */
215
429
  attachPoint: { xPx: number; yPx: number; space: GeometrySpace };
216
430
  /**
@@ -227,6 +441,12 @@ export interface EnvelopeBundle {
227
441
  * layer-05 closure.
228
442
  */
229
443
  linesCrossed: number;
444
+ /**
445
+ * Realized for live range scopes. Detached scopes are projected from a
446
+ * last-known range and are marked `requires-rehydration` so callers do not
447
+ * mistake the geometry for current document truth.
448
+ */
449
+ rehydrationStatus: GeometryRehydrationStatus;
230
450
  /**
231
451
  * Precision tag. `"exact"` when the envelope collapsed to a caret rect
232
452
  * (single anchor position); `"within-tolerance"` when the envelope is
@@ -13,7 +13,24 @@
13
13
 
14
14
  export type {
15
15
  GeometrySpace,
16
+ GeometryPrecision,
16
17
  GeometryRect,
18
+ GeometryRehydrationStatus,
19
+ GeometryPrecisionCounts,
20
+ GeometrySourceIdentityJoinKind,
21
+ GeometrySourceIdentity,
22
+ GeometryIndexCoverage,
23
+ GeometryIndex,
24
+ GeometryIndexPage,
25
+ GeometryIndexRegion,
26
+ GeometryIndexSlice,
27
+ GeometryIndexLine,
28
+ AnchorGeometry,
29
+ GeometryHitTarget,
30
+ SemanticDisplayEntryKind,
31
+ SemanticDisplayEntry,
32
+ GeometryReplacementEnvelopeEntry,
33
+ GeometryObjectHandleEntry,
17
34
  PageGeometry,
18
35
  BlockGeometry,
19
36
  LineGeometry,
@@ -24,11 +41,20 @@ export type {
24
41
  AnchorQueryKind,
25
42
  AnchorQuery,
26
43
  EnvelopeConfidence,
44
+ EnvelopeContinuationState,
27
45
  EnvelopeBundle,
28
46
  Viewport,
29
47
  ViewportListener,
30
48
  } from "./geometry-types.ts";
31
49
 
50
+ export {
51
+ createUnavailableGeometryCoverage,
52
+ projectGeometryIndexFromFrame,
53
+ summarizeGeometryCoverageFromFrame,
54
+ } from "./geometry-index.ts";
55
+
56
+ export type { GeometryIndexProjectionOptions } from "./geometry-index.ts";
57
+
32
58
  export type {
33
59
  GeometryFacet,
34
60
  CreateGeometryFacetInput,
@@ -10,6 +10,7 @@
10
10
 
11
11
  import type { GeometryFacet } from "./geometry-facet.ts";
12
12
  import type { Viewport } from "./geometry-types.ts";
13
+ import { createUnavailableGeometryCoverage } from "./geometry-index.ts";
13
14
 
14
15
  const INERT_VIEWPORT: Viewport = Object.freeze({
15
16
  scrollLeftPx: 0,
@@ -20,6 +21,8 @@ const INERT_VIEWPORT: Viewport = Object.freeze({
20
21
 
21
22
  export function createInertGeometryFacet(): GeometryFacet {
22
23
  return {
24
+ getGeometryIndex: () => null,
25
+ getGeometryCoverage: () => createUnavailableGeometryCoverage(),
23
26
  hitTest: () => null,
24
27
  getAnchorRects: () => [],
25
28
  getAnchor: () => null,
@@ -75,7 +75,7 @@ export function resolveReplacementEnvelope(
75
75
  if (!Number.isFinite(from) || !Number.isFinite(to)) return null;
76
76
  const rects = resolveSelectionRects(frame, { from, to, story });
77
77
  if (rects.length === 0) return null;
78
- return buildBundle(rects, "exact");
78
+ return buildBundle(frame, rects, "exact");
79
79
  }
80
80
 
81
81
  // Detached: project the last-known range and tag confidence as such.
@@ -83,10 +83,11 @@ export function resolveReplacementEnvelope(
83
83
  if (!Number.isFinite(from) || !Number.isFinite(to)) return null;
84
84
  const rects = resolveSelectionRects(frame, { from, to, story });
85
85
  if (rects.length === 0) return null;
86
- return buildBundle(rects, "detached");
86
+ return buildBundle(frame, rects, "detached");
87
87
  }
88
88
 
89
89
  function buildBundle(
90
+ frame: RenderFrame,
90
91
  rects: readonly GeometryRect[],
91
92
  confidence: EnvelopeBundle["confidence"],
92
93
  ): EnvelopeBundle {
@@ -104,8 +105,17 @@ function buildBundle(
104
105
  const rectPrecision = coarsestRectPrecision(rects);
105
106
  const envelopePrecision: EnvelopeBundle["precision"] =
106
107
  confidence === "detached" ? "heuristic" : rectPrecision;
108
+ const pageIds = resolveIntersectingPageIds(frame, rects);
107
109
  return {
108
110
  scopeRects: rects,
111
+ pageIds,
112
+ continuation: {
113
+ pageIds,
114
+ pageCount: pageIds.length,
115
+ crossesPageBoundary: pageIds.length > 1,
116
+ continuedFromPreviousPage: false,
117
+ continuesToNextPage: false,
118
+ },
109
119
  attachPoint: {
110
120
  xPx: first.leftPx,
111
121
  yPx: first.topPx,
@@ -113,10 +123,39 @@ function buildBundle(
113
123
  },
114
124
  confidence,
115
125
  linesCrossed: rects.length,
126
+ rehydrationStatus:
127
+ confidence === "detached" ? "requires-rehydration" : "realized",
116
128
  precision: envelopePrecision,
117
129
  };
118
130
  }
119
131
 
132
+ function resolveIntersectingPageIds(
133
+ frame: RenderFrame,
134
+ rects: readonly GeometryRect[],
135
+ ): readonly string[] {
136
+ if (!Array.isArray(frame.pages)) return [];
137
+ const ids: string[] = [];
138
+ for (const page of frame.pages) {
139
+ if (rects.some((rect) => intersects(rect, page.frame))) {
140
+ ids.push(page.page.pageId);
141
+ }
142
+ }
143
+ return ids;
144
+ }
145
+
146
+ function intersects(rect: GeometryRect, page: { leftPx: number; topPx: number; widthPx: number; heightPx: number }): boolean {
147
+ const rectRight = rect.leftPx + rect.widthPx;
148
+ const rectBottom = rect.topPx + rect.heightPx;
149
+ const pageRight = page.leftPx + page.widthPx;
150
+ const pageBottom = page.topPx + page.heightPx;
151
+ return (
152
+ rect.leftPx <= pageRight &&
153
+ rectRight >= page.leftPx &&
154
+ rect.topPx <= pageBottom &&
155
+ rectBottom >= page.topPx
156
+ );
157
+ }
158
+
120
159
  function coarsestRectPrecision(
121
160
  rects: readonly GeometryRect[],
122
161
  ): "exact" | "within-tolerance" | "heuristic" {
@@ -1031,8 +1031,23 @@
1031
1031
  * content instead of checking only top-level paragraphs. This honors
1032
1032
  * `<w:br w:type="page"/>` inside cover-page SDTs such as the CCEP SOW
1033
1033
  * template, changing page assignment from the v61 cache shape.
1034
+ *
1035
+ * 63 — PE2 geometry-projection substrate (Layer 05). Adds the new
1036
+ * geometry-index module (`src/runtime/geometry/geometry-index.ts`,
1037
+ * `geometry-facet.ts`, `caret-geometry.ts`, `replacement-envelope.ts`,
1038
+ * `inert-geometry-facet.ts`, `geometry-types.ts`, `index.ts`) and
1039
+ * advances the geometry projections that downstream caches consume.
1040
+ * Persisted layout caches keyed on the v62 geometry shape must
1041
+ * re-derive. Shipped via pe2 commits `957069161` + `1f36767d7`.
1042
+ *
1043
+ * 64 — PE2 Slice 2 page-frame graph substrate (Layer 04). `RuntimePageNode`
1044
+ * built by L04 now carries a twips-only `RuntimePageFrame` payload, explicit
1045
+ * `rectTwips` on resolved page regions, and typed layout divergence lists.
1046
+ * Existing `regions` remain the compatibility surface, but cache envelopes
1047
+ * from v63 invalidate because the page graph payload shape changed.
1048
+ * Shipped via pe2 commit `24a316af2`.
1034
1049
  */
1035
- export const LAYOUT_ENGINE_VERSION = 62 as const;
1050
+ export const LAYOUT_ENGINE_VERSION = 64 as const;
1036
1051
 
1037
1052
  /**
1038
1053
  * Serialization schema version for the LayCache payload (the cache envelope
@@ -48,6 +48,12 @@ import type { ResolvedDocumentSection } from "../document-layout.ts";
48
48
  export type {
49
49
  RuntimePageRegions,
50
50
  RuntimePageRegion,
51
+ RuntimeTwipsRect,
52
+ RuntimeResolvedRegions,
53
+ RuntimeExclusionZone,
54
+ RuntimeLayoutDivergenceKind,
55
+ RuntimeLayoutDivergence,
56
+ RuntimePageFrame,
51
57
  RuntimeBlockFragment,
52
58
  RuntimeLineBox,
53
59
  RuntimeNoteAllocation,
@@ -62,11 +68,15 @@ export type {
62
68
 
63
69
  import type {
64
70
  RuntimeBlockFragment,
71
+ RuntimeLayoutDivergence,
65
72
  RuntimeLineBox,
66
73
  RuntimeNoteAllocation,
67
74
  RuntimePageAnchor,
75
+ RuntimePageFrame,
68
76
  RuntimePageRegion,
69
77
  RuntimePageRegions,
78
+ RuntimeResolvedRegions,
79
+ RuntimeTwipsRect,
70
80
  } from "../../model/layout/page-graph-types.ts";
71
81
  import type {
72
82
  RuntimePageGraph,
@@ -164,6 +174,24 @@ export function buildPageGraph(
164
174
  input.noteAllocations?.get(pageId) ??
165
175
  [];
166
176
 
177
+ const frameId = buildPageFrameId(
178
+ page.pageIndex,
179
+ page.sectionIndex,
180
+ stories.displayPageNumber,
181
+ );
182
+ const regions = buildRegions(page.layout, bodyPageFragments, stories, pageNoteAllocations);
183
+ const divergences = detectFrameDivergences(frameId, regions);
184
+ const frame = buildPageFrame({
185
+ frameId,
186
+ pageId,
187
+ pageIndex: page.pageIndex,
188
+ sectionIndex: page.sectionIndex,
189
+ displayPageNumber: stories.displayPageNumber,
190
+ layout: page.layout,
191
+ regions,
192
+ divergences,
193
+ });
194
+
167
195
  const node: RuntimePageNode = {
168
196
  pageId,
169
197
  pageIndex: page.pageIndex,
@@ -173,7 +201,9 @@ export function buildPageGraph(
173
201
  endOffset: page.endOffset,
174
202
  layout: page.layout,
175
203
  stories,
176
- regions: buildRegions(page.layout, bodyPageFragments, stories, pageNoteAllocations),
204
+ regions,
205
+ frame,
206
+ divergences,
177
207
  lineBoxes:
178
208
  input.lineBoxesByPageIndex?.get(page.pageIndex) ??
179
209
  input.lineBoxesByPageIndex?.get(index) ??
@@ -240,6 +270,12 @@ function buildRegions(
240
270
  originTwips: layout.pageHeight - layout.marginBottom - totalNoteHeight,
241
271
  widthTwips: Math.max(0, bodyWidth),
242
272
  heightTwips: Math.max(0, totalNoteHeight),
273
+ rectTwips: rect(
274
+ layout.marginLeft,
275
+ layout.pageHeight - layout.marginBottom - totalNoteHeight,
276
+ Math.max(0, bodyWidth),
277
+ Math.max(0, totalNoteHeight),
278
+ ),
243
279
  fragmentIds,
244
280
  };
245
281
  bodyHeight = Math.max(0, bodyHeight - totalNoteHeight);
@@ -251,6 +287,7 @@ function buildRegions(
251
287
  originTwips: layout.marginTop,
252
288
  widthTwips: Math.max(0, bodyWidth),
253
289
  heightTwips: Math.max(0, bodyHeight),
290
+ rectTwips: rect(layout.marginLeft, layout.marginTop, Math.max(0, bodyWidth), Math.max(0, bodyHeight)),
254
291
  fragmentIds: [...bodyFragmentIds],
255
292
  };
256
293
 
@@ -262,6 +299,12 @@ function buildRegions(
262
299
  originTwips: layout.headerMargin ?? 720,
263
300
  widthTwips: Math.max(0, bodyWidth),
264
301
  heightTwips: Math.max(0, layout.marginTop - (layout.headerMargin ?? 720)),
302
+ rectTwips: rect(
303
+ layout.marginLeft,
304
+ layout.headerMargin ?? 720,
305
+ Math.max(0, bodyWidth),
306
+ Math.max(0, layout.marginTop - (layout.headerMargin ?? 720)),
307
+ ),
265
308
  fragmentIds: [],
266
309
  };
267
310
  }
@@ -271,6 +314,12 @@ function buildRegions(
271
314
  originTwips: layout.pageHeight - layout.marginBottom,
272
315
  widthTwips: Math.max(0, bodyWidth),
273
316
  heightTwips: Math.max(0, layout.marginBottom - (layout.footerMargin ?? 720)),
317
+ rectTwips: rect(
318
+ layout.marginLeft,
319
+ layout.pageHeight - layout.marginBottom,
320
+ Math.max(0, bodyWidth),
321
+ Math.max(0, layout.marginBottom - (layout.footerMargin ?? 720)),
322
+ ),
274
323
  fragmentIds: [],
275
324
  };
276
325
  }
@@ -299,6 +348,12 @@ function buildRegions(
299
348
  originTwips: layout.marginTop,
300
349
  widthTwips: perColumnWidth,
301
350
  heightTwips: Math.max(0, bodyHeight),
351
+ rectTwips: rect(
352
+ layout.marginLeft + i * (perColumnWidth + gap),
353
+ layout.marginTop,
354
+ perColumnWidth,
355
+ Math.max(0, bodyHeight),
356
+ ),
302
357
  fragmentIds: perColumnIds[i]!,
303
358
  });
304
359
  }
@@ -312,6 +367,141 @@ function buildRegions(
312
367
  return regions;
313
368
  }
314
369
 
370
+ function rect(
371
+ xTwips: number,
372
+ yTwips: number,
373
+ widthTwips: number,
374
+ heightTwips: number,
375
+ ): RuntimeTwipsRect {
376
+ return { xTwips, yTwips, widthTwips, heightTwips };
377
+ }
378
+
379
+ function buildPageFrame(input: {
380
+ frameId: string;
381
+ pageId: string;
382
+ pageIndex: number;
383
+ sectionIndex: number;
384
+ displayPageNumber: number;
385
+ layout: PageLayoutSnapshot;
386
+ regions: RuntimePageRegions;
387
+ divergences: readonly RuntimeLayoutDivergence[];
388
+ }): RuntimePageFrame {
389
+ const regions: RuntimeResolvedRegions = {
390
+ body: input.regions.body,
391
+ exclusionZones: [],
392
+ ...(input.regions.header ? { header: input.regions.header } : {}),
393
+ ...(input.regions.footer ? { footer: input.regions.footer } : {}),
394
+ ...(input.regions.columns ? { columns: input.regions.columns } : {}),
395
+ ...(input.regions.footnotes ? { footnotes: input.regions.footnotes } : {}),
396
+ };
397
+ const divergenceIds = input.divergences.map((d) => d.divergenceId);
398
+ return {
399
+ frameId: input.frameId,
400
+ pageId: input.pageId,
401
+ pageIndex: input.pageIndex,
402
+ sectionIndex: input.sectionIndex,
403
+ displayPageNumber: input.displayPageNumber,
404
+ physicalBoundsTwips: rect(0, 0, input.layout.pageWidth, input.layout.pageHeight),
405
+ regions,
406
+ divergenceIds,
407
+ signature: buildPageFrameSignature(input, divergenceIds),
408
+ };
409
+ }
410
+
411
+ function buildPageFrameId(
412
+ pageIndex: number,
413
+ sectionIndex: number,
414
+ displayPageNumber: number,
415
+ ): string {
416
+ return `page-frame-${pageIndex}-section-${sectionIndex}-display-${displayPageNumber}`;
417
+ }
418
+
419
+ function buildPageFrameSignature(
420
+ input: {
421
+ pageIndex: number;
422
+ sectionIndex: number;
423
+ displayPageNumber: number;
424
+ layout: PageLayoutSnapshot;
425
+ regions: RuntimePageRegions;
426
+ },
427
+ divergenceIds: readonly string[],
428
+ ): string {
429
+ const regionParts = collectRegions(input.regions).map((region) => {
430
+ const r = region.rectTwips ?? rect(0, region.originTwips, region.widthTwips, region.heightTwips);
431
+ return [
432
+ region.kind,
433
+ r.xTwips,
434
+ r.yTwips,
435
+ r.widthTwips,
436
+ r.heightTwips,
437
+ region.fragmentIds.length,
438
+ ].join(":");
439
+ });
440
+ return [
441
+ "page-frame",
442
+ input.pageIndex,
443
+ input.sectionIndex,
444
+ input.displayPageNumber,
445
+ input.layout.pageWidth,
446
+ input.layout.pageHeight,
447
+ ...regionParts,
448
+ ...divergenceIds,
449
+ ].join("|");
450
+ }
451
+
452
+ function detectFrameDivergences(
453
+ frameId: string,
454
+ regions: RuntimePageRegions,
455
+ ): RuntimeLayoutDivergence[] {
456
+ const candidates = collectRegions(regions).filter((region) => region.rectTwips !== undefined);
457
+ const divergences: RuntimeLayoutDivergence[] = [];
458
+ for (let i = 0; i < candidates.length; i += 1) {
459
+ for (let j = i + 1; j < candidates.length; j += 1) {
460
+ const a = candidates[i]!;
461
+ const b = candidates[j]!;
462
+ if (!shouldDetectCollision(a, b)) continue;
463
+ if (!rectsOverlap(a.rectTwips!, b.rectTwips!)) continue;
464
+ const kinds = [a.kind, b.kind].sort();
465
+ divergences.push({
466
+ divergenceId: `${frameId}:frame-collision:${kinds.join("-")}`,
467
+ kind: "frame-collision",
468
+ source: "runtime",
469
+ severity: "warning",
470
+ message: `Resolved page regions overlap: ${kinds.join(" / ")}`,
471
+ regionKinds: kinds as RuntimeLayoutDivergence["regionKinds"],
472
+ fragmentIds: [...a.fragmentIds, ...b.fragmentIds],
473
+ });
474
+ }
475
+ }
476
+ return divergences;
477
+ }
478
+
479
+ function collectRegions(regions: RuntimePageRegions): RuntimePageRegion[] {
480
+ return [
481
+ regions.body,
482
+ ...(regions.header ? [regions.header] : []),
483
+ ...(regions.footer ? [regions.footer] : []),
484
+ ...(regions.columns ?? []),
485
+ ...(regions.footnotes ?? []),
486
+ ];
487
+ }
488
+
489
+ function shouldDetectCollision(a: RuntimePageRegion, b: RuntimePageRegion): boolean {
490
+ if (a.kind === "body" && b.kind === "column") return false;
491
+ if (a.kind === "column" && b.kind === "body") return false;
492
+ if (a.kind === "column" && b.kind === "column") return false;
493
+ return true;
494
+ }
495
+
496
+ function rectsOverlap(a: RuntimeTwipsRect, b: RuntimeTwipsRect): boolean {
497
+ return (
498
+ a.xTwips < b.xTwips + b.widthTwips &&
499
+ a.xTwips + a.widthTwips > b.xTwips &&
500
+ a.yTwips < b.yTwips + b.heightTwips &&
501
+ a.yTwips + a.heightTwips > b.yTwips
502
+ );
503
+ }
504
+
315
505
  // ---------------------------------------------------------------------------
316
506
  // Graph queries
317
507
  // ---------------------------------------------------------------------------