@beyondwork/docx-react-component 1.0.104 → 1.0.106

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 (34) hide show
  1. package/package.json +1 -1
  2. package/src/api/public-types.ts +3 -0
  3. package/src/api/v3/_create.ts +9 -2
  4. package/src/api/v3/ai/_audit-reference.ts +28 -0
  5. package/src/api/v3/ai/_pe2-evidence.ts +419 -0
  6. package/src/api/v3/ai/attach.ts +22 -2
  7. package/src/api/v3/ai/bundle.ts +18 -6
  8. package/src/api/v3/ai/inspect.ts +12 -2
  9. package/src/api/v3/ai/replacement.ts +124 -0
  10. package/src/api/v3/index.ts +7 -0
  11. package/src/api/v3/ui/_types.ts +139 -0
  12. package/src/api/v3/ui/index.ts +9 -0
  13. package/src/api/v3/ui/overlays.ts +104 -0
  14. package/src/api/v3/ui/viewport.ts +97 -0
  15. package/src/model/layout/index.ts +3 -0
  16. package/src/model/layout/page-graph-types.ts +118 -0
  17. package/src/model/layout/runtime-page-graph-types.ts +13 -0
  18. package/src/runtime/document-runtime.ts +39 -18
  19. package/src/runtime/event-refresh-hints.ts +33 -6
  20. package/src/runtime/geometry/geometry-facet.ts +9 -1
  21. package/src/runtime/geometry/geometry-index.ts +461 -10
  22. package/src/runtime/geometry/geometry-types.ts +6 -0
  23. package/src/runtime/geometry/object-handles.ts +7 -4
  24. package/src/runtime/layout/layout-engine-instance.ts +2 -0
  25. package/src/runtime/layout/layout-engine-version.ts +36 -1
  26. package/src/runtime/layout/page-graph.ts +697 -10
  27. package/src/runtime/layout/paginated-layout-engine.ts +10 -0
  28. package/src/runtime/layout/project-block-fragments.ts +187 -8
  29. package/src/runtime/layout/public-facet.ts +236 -0
  30. package/src/runtime/prerender/graph-canonicalize.ts +14 -0
  31. package/src/runtime/workflow/index.ts +1 -0
  32. package/src/runtime/workflow/overlay-lanes.ts +228 -0
  33. package/src/ui/presence-overlay-lane.ts +131 -0
  34. package/src/ui/ui-controller-factory.ts +21 -0
@@ -22,6 +22,7 @@ import {
22
22
  MAIN_STORY_KEY,
23
23
  type CanonicalAnchorLayoutInput,
24
24
  type CanonicalLayoutInputs,
25
+ type CanonicalScopeLayoutInput,
25
26
  type CanonicalTableLayoutInput,
26
27
  type CanonicalTableCellLayoutInput,
27
28
  type CanonicalTableRowLayoutInput,
@@ -43,12 +44,15 @@ import type {
43
44
  GeometryIndexRegion,
44
45
  GeometryIndexSlice,
45
46
  GeometryObjectHandleEntry,
47
+ GeometryPrecision,
46
48
  GeometryPrecisionCounts,
47
49
  GeometryRect,
50
+ GeometryRehydrationStatus,
48
51
  GeometryReplacementEnvelopeEntry,
49
52
  GeometrySourceIdentity,
50
53
  SemanticDisplayEntry,
51
54
  } from "./geometry-types.ts";
55
+ import { buildObjectHandleRectsFromRect } from "./object-handles.ts";
52
56
 
53
57
  export interface GeometryIndexProjectionOptions {
54
58
  readonly canonicalDocument?: CanonicalDocument | null;
@@ -86,7 +90,8 @@ export function projectGeometryIndexFromFrame(
86
90
  const hitTargets: GeometryHitTarget[] = [];
87
91
  const semanticEntries: SemanticDisplayEntry[] = [];
88
92
  const replacementEnvelopes: GeometryReplacementEnvelopeEntry[] = [];
89
- const objectHandles: GeometryObjectHandleEntry[] = [];
93
+ const objectHandleEntries = new Map<string, MutableObjectHandleEntry>();
94
+ const projectedBlocksByStory = new Map<string, ProjectedScopeBlock[]>();
90
95
  const precision = createPrecisionCounts();
91
96
 
92
97
  for (const page of frame.pages) {
@@ -95,7 +100,9 @@ export function projectGeometryIndexFromFrame(
95
100
 
96
101
  for (const [region, ordinal] of regionEntries) {
97
102
  const regionId = makeRegionId(page, region, ordinal);
98
- const storyKey = canonicalStoryKeyFromTarget(region.storyTarget);
103
+ const storyKey =
104
+ identities?.storyKeyForTarget(region.storyTarget) ??
105
+ canonicalStoryKeyFromTarget(region.storyTarget);
99
106
  regionIds.push(regionId);
100
107
  const sliceIds: string[] = [];
101
108
 
@@ -200,6 +207,25 @@ export function projectGeometryIndexFromFrame(
200
207
  ...(sliceIdentity ? { sourceIdentity: sliceIdentity } : {}),
201
208
  });
202
209
  recordPrecision(precision, "exact");
210
+ recordProjectedScopeBlock(projectedBlocksByStory, {
211
+ storyKey,
212
+ blockId: block.fragment.blockId,
213
+ blockPath: identities?.blockPathForBlockId(
214
+ storyKey,
215
+ block.fragment.blockId,
216
+ ),
217
+ pageId: page.page.pageId,
218
+ rect: toGeometryRect(block.frame),
219
+ });
220
+ appendCanonicalObjectHandleEntries({
221
+ frame,
222
+ page,
223
+ block,
224
+ identities,
225
+ storyKey,
226
+ entries: objectHandleEntries,
227
+ precision,
228
+ });
203
229
  appendBlockSemanticEntries({
204
230
  page,
205
231
  region,
@@ -209,6 +235,7 @@ export function projectGeometryIndexFromFrame(
209
235
  identities,
210
236
  storyKey,
211
237
  entries: semanticEntries,
238
+ projectedBlocksByStory,
212
239
  precision,
213
240
  });
214
241
  }
@@ -232,8 +259,21 @@ export function projectGeometryIndexFromFrame(
232
259
  regionIds,
233
260
  });
234
261
  recordPrecision(precision, "exact");
262
+ appendPageLocalObjectHandleEntries({
263
+ page,
264
+ entries: objectHandleEntries,
265
+ precision,
266
+ });
235
267
  }
236
268
 
269
+ appendScopeReplacementEnvelopeEntries({
270
+ identities,
271
+ projectedBlocksByStory,
272
+ entries: replacementEnvelopes,
273
+ precision,
274
+ });
275
+
276
+ const objectHandles = finalizeObjectHandleEntries(objectHandleEntries);
237
277
  const coverage: GeometryIndexCoverage = {
238
278
  status: "realized",
239
279
  pageCount: pages.length,
@@ -336,6 +376,7 @@ function appendBlockSemanticEntries(input: {
336
376
  identities: GeometryIdentityLookup | null;
337
377
  storyKey: string;
338
378
  entries: SemanticDisplayEntry[];
379
+ projectedBlocksByStory: Map<string, ProjectedScopeBlock[]>;
339
380
  precision: GeometryPrecisionCounts;
340
381
  }): void {
341
382
  const {
@@ -347,6 +388,7 @@ function appendBlockSemanticEntries(input: {
347
388
  identities,
348
389
  storyKey,
349
390
  entries,
391
+ projectedBlocksByStory,
350
392
  precision,
351
393
  } = input;
352
394
  const tableIdentity = identities?.tableIdentity(
@@ -379,6 +421,16 @@ function appendBlockSemanticEntries(input: {
379
421
 
380
422
  const plan = block.tablePlan;
381
423
  if (!plan) return;
424
+ if (tableIdentity) {
425
+ recordTableCellScopeBlocks({
426
+ table: tableIdentity,
427
+ block,
428
+ pageId: page.page.pageId,
429
+ storyKey,
430
+ identities,
431
+ projectedBlocksByStory,
432
+ });
433
+ }
382
434
  const rowCount = resolveTableRowCount(block);
383
435
  const rowHeightPx =
384
436
  rowCount > 0 ? block.frame.heightPx / rowCount : block.frame.heightPx;
@@ -551,9 +603,14 @@ function pageFrameCellRect(
551
603
  }
552
604
 
553
605
  interface GeometryIdentityLookup {
606
+ storyKeyForTarget(target: EditorStoryTarget): string;
554
607
  sliceIdentity(storyKey: string, blockId: string): GeometrySourceIdentity | undefined;
555
608
  tableIdentity(storyKey: string, blockId: string): CanonicalTableLayoutInput | undefined;
556
609
  anchorIdentity(storyKey: string, blockId: string): GeometrySourceIdentity | undefined;
610
+ objectAnchors(storyKey: string, blockId: string): readonly CanonicalAnchorLayoutInput[];
611
+ blockIdForPath(storyKey: string, blockPath: string): string | undefined;
612
+ blockPathForBlockId(storyKey: string, blockId: string): string | undefined;
613
+ scopeInputs(): readonly CanonicalScopeLayoutInput[];
557
614
  }
558
615
 
559
616
  function createIdentityLookup(
@@ -566,6 +623,7 @@ function createIdentityLookup(
566
623
  : null);
567
624
  if (!layoutInputs) return null;
568
625
 
626
+ const storyKeys = new Set(layoutInputs.stories.map((story) => story.storyKey));
569
627
  const blockIdByPath = options?.canonicalDocument
570
628
  ? collectProjectedBlockIdsByPath(options.canonicalDocument)
571
629
  : new Map<string, string>();
@@ -603,6 +661,9 @@ function createIdentityLookup(
603
661
  }
604
662
 
605
663
  return {
664
+ storyKeyForTarget(target) {
665
+ return resolveCanonicalStoryKeyForTarget(target, storyKeys);
666
+ },
606
667
  sliceIdentity(storyKey, blockId) {
607
668
  const table = tableByStoryBlockId.get(storyBlockKey(storyKey, blockId));
608
669
  if (table) return tableSourceIdentity(table);
@@ -627,16 +688,406 @@ function createIdentityLookup(
627
688
  };
628
689
  }
629
690
  const anchor = anchors[0]!;
630
- return {
631
- storyKey,
632
- blockPath: anchor.blockPath,
633
- objectKey: anchor.objectKey,
634
- inlinePath: anchor.inlinePath,
635
- objectKind: anchor.objectKind,
636
- editPosture: anchor.editPosture,
691
+ return anchorSourceIdentity(anchor, "block-scoped");
692
+ },
693
+ objectAnchors(storyKey, blockId) {
694
+ return anchorsByStoryBlockId.get(storyBlockKey(storyKey, blockId)) ?? [];
695
+ },
696
+ blockIdForPath(storyKey, blockPath) {
697
+ return blockIdByPath.get(storyPathKey(storyKey, blockPath));
698
+ },
699
+ blockPathForBlockId(storyKey, blockId) {
700
+ return blockPathByStoryBlockId.get(storyBlockKey(storyKey, blockId));
701
+ },
702
+ scopeInputs() {
703
+ return layoutInputs.scopes;
704
+ },
705
+ };
706
+ }
707
+
708
+ interface MutableObjectHandleEntry {
709
+ objectId: string;
710
+ pageIds: string[];
711
+ rects: GeometryRect[];
712
+ status: GeometryRehydrationStatus;
713
+ precision: GeometryPrecision;
714
+ sourceIdentity?: GeometrySourceIdentity;
715
+ }
716
+
717
+ function appendCanonicalObjectHandleEntries(input: {
718
+ frame: RenderFrame;
719
+ page: RenderPage;
720
+ block: RenderBlock;
721
+ identities: GeometryIdentityLookup | null;
722
+ storyKey: string;
723
+ entries: Map<string, MutableObjectHandleEntry>;
724
+ precision: GeometryPrecisionCounts;
725
+ }): void {
726
+ const { frame, page, block, identities, storyKey, entries, precision } = input;
727
+ if (!identities) return;
728
+
729
+ for (const anchor of identities.objectAnchors(
730
+ storyKey,
731
+ block.fragment.blockId,
732
+ )) {
733
+ if (anchor.hidden === true) continue;
734
+ const exactObjectRect = frame.anchorIndex.byObjectId(anchor.objectKey);
735
+ const rect = exactObjectRect ?? block.frame;
736
+ const entryPrecision: GeometryPrecision = exactObjectRect
737
+ ? "within-tolerance"
738
+ : "heuristic";
739
+ const status: GeometryRehydrationStatus = exactObjectRect
740
+ ? "realized"
741
+ : "requires-rehydration";
742
+ const handleRects = buildObjectHandleRectsFromRect(rect, entryPrecision);
743
+ const existing = entries.get(anchor.objectKey);
744
+ if (existing) {
745
+ appendUnique(existing.pageIds, page.page.pageId);
746
+ existing.rects.push(...handleRects);
747
+ if (existing.precision !== "heuristic" && entryPrecision === "heuristic") {
748
+ existing.precision = "heuristic";
749
+ existing.status = "requires-rehydration";
750
+ existing.sourceIdentity = anchorSourceIdentity(anchor, "block-scoped");
751
+ }
752
+ continue;
753
+ }
754
+
755
+ entries.set(anchor.objectKey, {
756
+ objectId: anchor.objectKey,
757
+ pageIds: [page.page.pageId],
758
+ rects: [...handleRects],
759
+ status,
760
+ precision: entryPrecision,
761
+ sourceIdentity: anchorSourceIdentity(
762
+ anchor,
763
+ exactObjectRect ? "direct" : "block-scoped",
764
+ ),
765
+ });
766
+ recordPrecision(precision, entryPrecision);
767
+ }
768
+ }
769
+
770
+ function appendPageLocalObjectHandleEntries(input: {
771
+ page: RenderPage;
772
+ entries: Map<string, MutableObjectHandleEntry>;
773
+ precision: GeometryPrecisionCounts;
774
+ }): void {
775
+ const { page, entries, precision } = input;
776
+ const stories = page.page.frame?.pageLocalStories ?? [];
777
+ for (const story of stories) {
778
+ const regionFrame =
779
+ story.kind === "header" ? page.regions.header?.frame : page.regions.footer?.frame;
780
+ if (!regionFrame) continue;
781
+ for (const object of story.anchoredObjects) {
782
+ const objectFrame = pageLocalObjectFrame(
783
+ regionFrame,
784
+ object.extentTwips,
785
+ page.page.layout.pageWidth > 0
786
+ ? page.frame.widthPx / page.page.layout.pageWidth
787
+ : 1,
788
+ );
789
+ const handleRects = buildObjectHandleRectsFromRect(objectFrame, "heuristic");
790
+ const sourceIdentity: GeometrySourceIdentity = {
791
+ storyKey: story.storyKey,
792
+ objectKey: object.objectId,
793
+ objectKind: object.sourceType,
794
+ editPosture: object.preserveOnly ? "preserve-only" : "editable",
637
795
  joinKind: "block-scoped",
638
796
  };
639
- },
797
+ const existing = entries.get(object.objectId);
798
+ if (existing) {
799
+ appendUnique(existing.pageIds, page.page.pageId);
800
+ existing.rects.push(...handleRects);
801
+ if (existing.precision !== "heuristic") {
802
+ existing.precision = "heuristic";
803
+ existing.status = "requires-rehydration";
804
+ existing.sourceIdentity = sourceIdentity;
805
+ }
806
+ continue;
807
+ }
808
+ entries.set(object.objectId, {
809
+ objectId: object.objectId,
810
+ pageIds: [page.page.pageId],
811
+ rects: [...handleRects],
812
+ status: "requires-rehydration",
813
+ precision: "heuristic",
814
+ sourceIdentity,
815
+ });
816
+ recordPrecision(precision, "heuristic");
817
+ }
818
+ }
819
+ }
820
+
821
+ function pageLocalObjectFrame(
822
+ regionFrame: RenderFrameRect,
823
+ extentTwips:
824
+ | { readonly widthTwips: number; readonly heightTwips: number }
825
+ | undefined,
826
+ pxPerTwip: number,
827
+ ): RenderFrameRect {
828
+ if (!extentTwips) return regionFrame;
829
+ const widthPx = Math.max(0, extentTwips.widthTwips * pxPerTwip);
830
+ const heightPx = Math.max(0, extentTwips.heightTwips * pxPerTwip);
831
+ return {
832
+ leftPx: regionFrame.leftPx,
833
+ topPx: regionFrame.topPx,
834
+ widthPx: widthPx > 0 ? Math.min(widthPx, regionFrame.widthPx) : regionFrame.widthPx,
835
+ heightPx: heightPx > 0 ? Math.min(heightPx, regionFrame.heightPx) : regionFrame.heightPx,
836
+ };
837
+ }
838
+
839
+ function finalizeObjectHandleEntries(
840
+ entries: ReadonlyMap<string, MutableObjectHandleEntry>,
841
+ ): GeometryObjectHandleEntry[] {
842
+ return Array.from(entries.values()).map((entry) => ({
843
+ objectId: entry.objectId,
844
+ pageIds: entry.pageIds,
845
+ rects: entry.rects,
846
+ status: entry.status,
847
+ precision: entry.precision,
848
+ ...(entry.sourceIdentity ? { sourceIdentity: entry.sourceIdentity } : {}),
849
+ }));
850
+ }
851
+
852
+ function appendUnique(target: string[], value: string): void {
853
+ if (!target.includes(value)) target.push(value);
854
+ }
855
+
856
+ function resolveCanonicalStoryKeyForTarget(
857
+ target: EditorStoryTarget,
858
+ storyKeys: ReadonlySet<string>,
859
+ ): string {
860
+ const exact = canonicalStoryKeyFromTarget(target);
861
+ if (storyKeys.has(exact)) return exact;
862
+ if (
863
+ (target.kind === "header" || target.kind === "footer") &&
864
+ target.sectionIndex !== undefined
865
+ ) {
866
+ const unscoped = createHeaderFooterStoryKey({
867
+ kind: target.kind,
868
+ relationshipId: target.relationshipId,
869
+ variant: target.variant,
870
+ });
871
+ if (storyKeys.has(unscoped)) return unscoped;
872
+ }
873
+ return exact;
874
+ }
875
+
876
+ interface ProjectedScopeBlock {
877
+ readonly storyKey: string;
878
+ readonly blockId: string;
879
+ readonly blockPath?: string;
880
+ readonly pageId: string;
881
+ readonly rect: GeometryRect;
882
+ }
883
+
884
+ function recordProjectedScopeBlock(
885
+ blocksByStory: Map<string, ProjectedScopeBlock[]>,
886
+ block: ProjectedScopeBlock,
887
+ ): void {
888
+ const list = blocksByStory.get(block.storyKey);
889
+ if (list) {
890
+ list.push(block);
891
+ } else {
892
+ blocksByStory.set(block.storyKey, [block]);
893
+ }
894
+ }
895
+
896
+ function recordTableCellScopeBlocks(input: {
897
+ table: CanonicalTableLayoutInput;
898
+ block: RenderBlock;
899
+ pageId: string;
900
+ storyKey: string;
901
+ identities: GeometryIdentityLookup | null;
902
+ projectedBlocksByStory: Map<string, ProjectedScopeBlock[]>;
903
+ }): void {
904
+ const {
905
+ table,
906
+ block,
907
+ pageId,
908
+ storyKey,
909
+ identities,
910
+ projectedBlocksByStory,
911
+ } = input;
912
+ if (!identities) return;
913
+
914
+ for (const row of table.rows) {
915
+ for (const cell of row.cells) {
916
+ const rect = pageFrameCellRect(block, row.rowIndex, cell.gridColumnStart);
917
+ if (!rect) continue;
918
+ for (let blockIndex = 0; blockIndex < cell.blockCount; blockIndex += 1) {
919
+ const blockPath =
920
+ `${table.blockPath}/row[${row.rowIndex}]/cell[${cell.cellIndex}]` +
921
+ `/block[${blockIndex}]`;
922
+ const blockId = identities.blockIdForPath(storyKey, blockPath);
923
+ if (!blockId) continue;
924
+ recordProjectedScopeBlock(projectedBlocksByStory, {
925
+ storyKey,
926
+ blockId,
927
+ blockPath,
928
+ pageId,
929
+ rect: {
930
+ ...toGeometryRect(rect),
931
+ precision: "within-tolerance",
932
+ },
933
+ });
934
+ }
935
+ }
936
+ }
937
+ }
938
+
939
+ function appendScopeReplacementEnvelopeEntries(input: {
940
+ identities: GeometryIdentityLookup | null;
941
+ projectedBlocksByStory: ReadonlyMap<string, readonly ProjectedScopeBlock[]>;
942
+ entries: GeometryReplacementEnvelopeEntry[];
943
+ precision: GeometryPrecisionCounts;
944
+ }): void {
945
+ const { identities, projectedBlocksByStory, entries, precision } = input;
946
+ if (!identities) return;
947
+
948
+ for (const scope of identities.scopeInputs()) {
949
+ const entry = projectScopeReplacementEnvelopeEntry(
950
+ scope,
951
+ identities,
952
+ projectedBlocksByStory.get(scope.storyKey) ?? [],
953
+ );
954
+ entries.push(entry);
955
+ recordPrecision(precision, entry.precision);
956
+ }
957
+ }
958
+
959
+ function projectScopeReplacementEnvelopeEntry(
960
+ scope: CanonicalScopeLayoutInput,
961
+ identities: GeometryIdentityLookup,
962
+ storyBlocks: readonly ProjectedScopeBlock[],
963
+ ): GeometryReplacementEnvelopeEntry {
964
+ const startBlockId = scope.start
965
+ ? identities.blockIdForPath(scope.storyKey, scope.start.blockPath)
966
+ : undefined;
967
+ const endBlockId = scope.end
968
+ ? identities.blockIdForPath(scope.storyKey, scope.end.blockPath)
969
+ : undefined;
970
+
971
+ let blocks: readonly ProjectedScopeBlock[] = [];
972
+ let status: GeometryReplacementEnvelopeEntry["status"] = "unavailable";
973
+ let precision: GeometryReplacementEnvelopeEntry["precision"] = "heuristic";
974
+
975
+ if (scope.status === "paired" && startBlockId && endBlockId) {
976
+ blocks = blocksInCanonicalPathRange(scope, storyBlocks);
977
+ if (blocks.length === 0) {
978
+ const startIndex = firstBlockIndex(storyBlocks, startBlockId);
979
+ const endIndex = lastBlockIndex(storyBlocks, endBlockId);
980
+ if (startIndex >= 0 && endIndex >= 0) {
981
+ const from = Math.min(startIndex, endIndex);
982
+ const to = Math.max(startIndex, endIndex);
983
+ blocks = storyBlocks.slice(from, to + 1);
984
+ }
985
+ }
986
+ if (blocks.length > 0) {
987
+ status = "realized";
988
+ precision = "within-tolerance";
989
+ }
990
+ }
991
+
992
+ if (blocks.length === 0) {
993
+ const fallbackBlockId = startBlockId ?? endBlockId;
994
+ if (fallbackBlockId) {
995
+ blocks = storyBlocks.filter((block) => block.blockId === fallbackBlockId);
996
+ if (blocks.length > 0) status = "requires-rehydration";
997
+ }
998
+ }
999
+
1000
+ return {
1001
+ scopeId: scope.scopeId,
1002
+ pageIds: uniquePageIds(blocks),
1003
+ rects: blocks.map((block) => ({
1004
+ ...block.rect,
1005
+ precision,
1006
+ })),
1007
+ status,
1008
+ precision,
1009
+ sourceIdentity: scopeSourceIdentity(scope),
1010
+ };
1011
+ }
1012
+
1013
+ function blocksInCanonicalPathRange(
1014
+ scope: CanonicalScopeLayoutInput,
1015
+ storyBlocks: readonly ProjectedScopeBlock[],
1016
+ ): readonly ProjectedScopeBlock[] {
1017
+ if (!scope.start || !scope.end) return [];
1018
+ const orderByPath = new Map<string, number>();
1019
+ for (const block of storyBlocks) {
1020
+ if (!block.blockPath || orderByPath.has(block.blockPath)) continue;
1021
+ orderByPath.set(block.blockPath, orderByPath.size);
1022
+ }
1023
+ const startOrder = orderByPath.get(scope.start.blockPath);
1024
+ const endOrder = orderByPath.get(scope.end.blockPath);
1025
+ if (startOrder === undefined || endOrder === undefined) return [];
1026
+
1027
+ const from = Math.min(startOrder, endOrder);
1028
+ const to = Math.max(startOrder, endOrder);
1029
+ return storyBlocks.filter((block) => {
1030
+ if (!block.blockPath) return false;
1031
+ const order = orderByPath.get(block.blockPath);
1032
+ return order !== undefined && order >= from && order <= to;
1033
+ });
1034
+ }
1035
+
1036
+ function firstBlockIndex(
1037
+ blocks: readonly ProjectedScopeBlock[],
1038
+ blockId: string,
1039
+ ): number {
1040
+ return blocks.findIndex((block) => block.blockId === blockId);
1041
+ }
1042
+
1043
+ function lastBlockIndex(
1044
+ blocks: readonly ProjectedScopeBlock[],
1045
+ blockId: string,
1046
+ ): number {
1047
+ for (let index = blocks.length - 1; index >= 0; index -= 1) {
1048
+ if (blocks[index]?.blockId === blockId) return index;
1049
+ }
1050
+ return -1;
1051
+ }
1052
+
1053
+ function uniquePageIds(
1054
+ blocks: readonly ProjectedScopeBlock[],
1055
+ ): readonly string[] {
1056
+ const ids: string[] = [];
1057
+ const seen = new Set<string>();
1058
+ for (const block of blocks) {
1059
+ if (seen.has(block.pageId)) continue;
1060
+ seen.add(block.pageId);
1061
+ ids.push(block.pageId);
1062
+ }
1063
+ return ids;
1064
+ }
1065
+
1066
+ function scopeSourceIdentity(
1067
+ scope: CanonicalScopeLayoutInput,
1068
+ ): GeometrySourceIdentity {
1069
+ const marker = scope.start ?? scope.end;
1070
+ return {
1071
+ storyKey: scope.storyKey,
1072
+ ...(marker ? { blockPath: marker.blockPath } : {}),
1073
+ scopeKey: scope.scopeKey,
1074
+ scopeId: scope.scopeId,
1075
+ joinKind: "block-scoped",
1076
+ };
1077
+ }
1078
+
1079
+ function anchorSourceIdentity(
1080
+ anchor: CanonicalAnchorLayoutInput,
1081
+ joinKind: GeometrySourceIdentity["joinKind"],
1082
+ ): GeometrySourceIdentity {
1083
+ return {
1084
+ storyKey: anchor.storyKey,
1085
+ blockPath: anchor.blockPath,
1086
+ objectKey: anchor.objectKey,
1087
+ inlinePath: anchor.inlinePath,
1088
+ objectKind: anchor.objectKind,
1089
+ editPosture: anchor.editPosture,
1090
+ joinKind,
640
1091
  };
641
1092
  }
642
1093
 
@@ -147,6 +147,8 @@ export interface GeometrySourceIdentity {
147
147
  tableKey?: string;
148
148
  rowKey?: string;
149
149
  cellKey?: string;
150
+ scopeKey?: string;
151
+ scopeId?: string;
150
152
  objectKey?: string;
151
153
  inlinePath?: string;
152
154
  objectKind?: string;
@@ -288,16 +290,20 @@ export interface SemanticDisplayEntry {
288
290
 
289
291
  export interface GeometryReplacementEnvelopeEntry {
290
292
  scopeId: string;
293
+ pageIds?: readonly string[];
291
294
  rects: readonly GeometryRect[];
292
295
  status: GeometryRehydrationStatus;
293
296
  precision: GeometryPrecision;
297
+ sourceIdentity?: GeometrySourceIdentity;
294
298
  }
295
299
 
296
300
  export interface GeometryObjectHandleEntry {
297
301
  objectId: string;
302
+ pageIds?: readonly string[];
298
303
  rects: readonly GeometryRect[];
299
304
  status: GeometryRehydrationStatus;
300
305
  precision: GeometryPrecision;
306
+ sourceIdentity?: GeometrySourceIdentity;
301
307
  }
302
308
 
303
309
  // ---------------------------------------------------------------------------
@@ -27,7 +27,7 @@
27
27
  */
28
28
 
29
29
  import type { RenderFrame, RenderFrameRect } from "../render/index.ts";
30
- import type { GeometryRect } from "./geometry-types.ts";
30
+ import type { GeometryPrecision, GeometryRect } from "./geometry-types.ts";
31
31
 
32
32
  const ROTATE_HANDLE_OFFSET_PX = 20;
33
33
 
@@ -38,10 +38,13 @@ export function resolveObjectHandles(
38
38
  if (!frame) return [];
39
39
  const bbox = frame.anchorIndex.byObjectId(objectId);
40
40
  if (!bbox) return [];
41
- return buildHandles(bbox);
41
+ return buildObjectHandleRectsFromRect(bbox);
42
42
  }
43
43
 
44
- function buildHandles(bbox: RenderFrameRect): readonly GeometryRect[] {
44
+ export function buildObjectHandleRectsFromRect(
45
+ bbox: RenderFrameRect,
46
+ precision: GeometryPrecision = "heuristic",
47
+ ): readonly GeometryRect[] {
45
48
  const { leftPx, topPx, widthPx, heightPx } = bbox;
46
49
  const right = leftPx + widthPx;
47
50
  const bottom = topPx + heightPx;
@@ -61,7 +64,7 @@ function buildHandles(bbox: RenderFrameRect): readonly GeometryRect[] {
61
64
  widthPx: 0,
62
65
  heightPx: 0,
63
66
  space: "frame",
64
- precision: "heuristic",
67
+ precision,
65
68
  });
66
69
  return [
67
70
  point(leftPx, topPx), // 0 top-left
@@ -490,6 +490,7 @@ export function createLayoutEngine(
490
490
  fragmentsByPageIndex,
491
491
  lineBoxesByPageIndex,
492
492
  noteAllocationsByPageIndex: pageStack.noteAllocationsByPageIndex,
493
+ subParts: document.subParts,
493
494
  });
494
495
 
495
496
  // Field dirtiness diff from previous graph
@@ -669,6 +670,7 @@ export function createLayoutEngine(
669
670
  fragmentsByPageIndex: freshFragmentsByPageIndex,
670
671
  lineBoxesByPageIndex: freshLineBoxesByPageIndex,
671
672
  noteAllocationsByPageIndex: freshResult.noteAllocationsByPageIndex,
673
+ subParts: document.subParts,
672
674
  });
673
675
  const freshNodes = freshGraph.pages;
674
676
 
@@ -1046,8 +1046,43 @@
1046
1046
  * Existing `regions` remain the compatibility surface, but cache envelopes
1047
1047
  * from v63 invalidate because the page graph payload shape changed.
1048
1048
  * Shipped via pe2 commit `24a316af2`.
1049
+ *
1050
+ * 65 — PE2 Slice 2 page-local story instances (Layer 04). `RuntimePageFrame`
1051
+ * now carries stable header/footer `pageLocalStories` records with story
1052
+ * keys, variants, relationship ids, measured frame heights, and empty
1053
+ * field/object ledgers for later Slice 2/4 population. Cache envelopes
1054
+ * from v64 invalidate because the page-frame payload shape changed again.
1055
+ * Shipped via pe2 commit `06fe6f692`.
1056
+ *
1057
+ * 66 — PE2 Slice 2 page-local field ledgers (Layer 04). Page graph
1058
+ * construction now walks canonical header/footer subparts for each
1059
+ * page-local story instance and records resolved PAGE / NUMPAGES /
1060
+ * SECTIONPAGES display text in the story's `resolvedFields` ledger.
1061
+ * Cache envelopes from v65 invalidate because page-frame story payloads
1062
+ * now carry field-resolution data. Shipped via pe2 commit `a3a42bec9`.
1063
+ *
1064
+ * 67 — PE2 Slice 2 page-local object ledgers (Layer 04). Header/footer
1065
+ * page-local story instances now record canonical image/drawing/preserve-only
1066
+ * objects as twips/plain anchored-object ledger rows and emit typed
1067
+ * unsupported-wrap / preserve-only-placeholder divergences. Cache envelopes
1068
+ * from v66 invalidate because page-frame story payloads and divergence ids
1069
+ * can change. Shipped via pe2 commit `7d8bb94ac`.
1070
+ *
1071
+ * 68 — PE2 Slice 3 typed continuation cursors (Layer 04). Paragraph and
1072
+ * table slice fragments now carry a twips/plain continuation cursor with
1073
+ * sequence position, line/row range, continuation direction, repeated
1074
+ * table header rows, and vertical-merge carry metadata. Cache envelopes
1075
+ * from v67 invalidate because fragment payloads can now include
1076
+ * continuation state. Shipped via pe2 commit `33fbf45ac`.
1077
+ *
1078
+ * 69 — PE2 Slice 3 measured layout-object descriptors (Layer 04). Body
1079
+ * fragments and footnote-area fragments now carry a twips/plain
1080
+ * `layoutObject` row with stable object id, source block id, object kind,
1081
+ * pagination role, measured extent, and field-family hints where applicable.
1082
+ * Cache envelopes from v68 invalidate because fragment payloads can now
1083
+ * include layout-object descriptors. Shipped via pe2 commit `9c4417418`.
1049
1084
  */
1050
- export const LAYOUT_ENGINE_VERSION = 64 as const;
1085
+ export const LAYOUT_ENGINE_VERSION = 69 as const;
1051
1086
 
1052
1087
  /**
1053
1088
  * Serialization schema version for the LayCache payload (the cache envelope