@beyondwork/docx-react-component 1.0.105 → 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.
- package/package.json +1 -1
- package/src/api/v3/_create.ts +9 -2
- package/src/api/v3/ai/_audit-reference.ts +28 -0
- package/src/api/v3/ai/_pe2-evidence.ts +272 -6
- package/src/api/v3/ai/attach.ts +22 -2
- package/src/api/v3/ai/bundle.ts +6 -2
- package/src/api/v3/ai/inspect.ts +6 -2
- package/src/api/v3/ai/replacement.ts +11 -0
- package/src/api/v3/index.ts +7 -0
- package/src/api/v3/ui/_types.ts +53 -0
- package/src/api/v3/ui/index.ts +4 -0
- package/src/api/v3/ui/viewport.ts +97 -0
- package/src/model/layout/page-graph-types.ts +29 -0
- package/src/runtime/document-runtime.ts +39 -18
- package/src/runtime/geometry/geometry-index.ts +74 -0
- package/src/runtime/layout/layout-engine-version.ts +8 -1
- package/src/runtime/layout/page-graph.ts +2 -0
- package/src/runtime/layout/paginated-layout-engine.ts +10 -0
- package/src/runtime/layout/project-block-fragments.ts +86 -7
- package/src/runtime/layout/public-facet.ts +84 -0
- package/src/runtime/workflow/index.ts +1 -0
- package/src/runtime/workflow/overlay-lanes.ts +228 -0
- package/src/ui/presence-overlay-lane.ts +131 -0
- package/src/ui/ui-controller-factory.ts +10 -0
package/src/api/v3/ui/_types.ts
CHANGED
|
@@ -118,6 +118,21 @@ export interface UiController {
|
|
|
118
118
|
* error rather than silently no-op.
|
|
119
119
|
*/
|
|
120
120
|
readonly subscribeViewport?: (listener: UiListener<ViewportState>) => UiUnsubscribe;
|
|
121
|
+
/**
|
|
122
|
+
* PE2 page-residency policy read. L10 owns the observable residency
|
|
123
|
+
* shape; L11 owns realized DOM/PM attachment and L05 owns geometry
|
|
124
|
+
* caches. Omitting the hook makes `ui.viewport.getPageResidency`
|
|
125
|
+
* return an explicit unavailable snapshot.
|
|
126
|
+
*/
|
|
127
|
+
readonly getPageResidency?: (pageIndex: number) => PageResidencySnapshot;
|
|
128
|
+
/**
|
|
129
|
+
* Page-residency subscription. Fires when the mounted surface changes
|
|
130
|
+
* whether a page is realized, cold, or evicted.
|
|
131
|
+
*/
|
|
132
|
+
readonly subscribePageResidency?: (
|
|
133
|
+
pageIndex: number,
|
|
134
|
+
listener: UiListener<PageResidencySnapshot>,
|
|
135
|
+
) => UiUnsubscribe;
|
|
121
136
|
/**
|
|
122
137
|
* Overlay invalidation subscription. Fires when geometry invalidation
|
|
123
138
|
* ranges overlap attached overlay queries (U7). `ui.overlays.subscribe`
|
|
@@ -234,6 +249,29 @@ export interface ViewportState {
|
|
|
234
249
|
readonly devicePixelRatio: number;
|
|
235
250
|
}
|
|
236
251
|
|
|
252
|
+
export type PageResidency = "realized" | "cold" | "evicted";
|
|
253
|
+
|
|
254
|
+
export type RehydrationStatus =
|
|
255
|
+
| "available"
|
|
256
|
+
| "requires-rehydration"
|
|
257
|
+
| "unavailable";
|
|
258
|
+
|
|
259
|
+
export type PageResidencySource =
|
|
260
|
+
| "controller"
|
|
261
|
+
| "unavailable";
|
|
262
|
+
|
|
263
|
+
export interface PageResidencySnapshot {
|
|
264
|
+
readonly __mock?: true;
|
|
265
|
+
/** 0-based page index, matching GeometryFacet.getPage(index). */
|
|
266
|
+
readonly pageIndex: number;
|
|
267
|
+
readonly pageId?: string;
|
|
268
|
+
readonly residency: PageResidency;
|
|
269
|
+
readonly status: RehydrationStatus;
|
|
270
|
+
readonly revision: number;
|
|
271
|
+
readonly source: PageResidencySource;
|
|
272
|
+
readonly reason?: string;
|
|
273
|
+
}
|
|
274
|
+
|
|
237
275
|
export type ScrollTargetBehavior = "auto" | "smooth" | "instant";
|
|
238
276
|
|
|
239
277
|
export type ScrollTarget =
|
|
@@ -452,6 +490,21 @@ export interface ApiV3UiSurface {
|
|
|
452
490
|
export interface ApiV3UiViewport {
|
|
453
491
|
get(): ViewportState;
|
|
454
492
|
subscribe(listener: UiListener<ViewportState>): UiUnsubscribe;
|
|
493
|
+
/**
|
|
494
|
+
* Read L10's page-residency policy for a 0-based page index. When no
|
|
495
|
+
* mounted controller has supplied a residency policy yet, returns an
|
|
496
|
+
* explicit `unavailable` mock snapshot instead of probing geometry or
|
|
497
|
+
* realizing DOM.
|
|
498
|
+
*/
|
|
499
|
+
getPageResidency(pageIndex: number): PageResidencySnapshot;
|
|
500
|
+
/**
|
|
501
|
+
* Subscribe to residency changes for a 0-based page index. The listener
|
|
502
|
+
* receives plain snapshots; realization/teardown stays owned by L11.
|
|
503
|
+
*/
|
|
504
|
+
subscribePageResidency(
|
|
505
|
+
pageIndex: number,
|
|
506
|
+
listener: UiListener<PageResidencySnapshot>,
|
|
507
|
+
): UiUnsubscribe;
|
|
455
508
|
|
|
456
509
|
/**
|
|
457
510
|
* Scroll the mounted surface to a specific 1-based page number.
|
package/src/api/v3/ui/index.ts
CHANGED
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
import type { ApiV3FnMetadata } from "../_layer-metadata.ts";
|
|
25
25
|
import type {
|
|
26
26
|
ViewportState,
|
|
27
|
+
PageResidencySnapshot,
|
|
27
28
|
UiListener,
|
|
28
29
|
UiUnsubscribe,
|
|
29
30
|
WorkflowMarkupMode,
|
|
@@ -86,6 +87,52 @@ export const subscribeMetadata: ApiV3FnMetadata = {
|
|
|
86
87
|
rwdReference: "§UI API § ui.viewport.subscribe. Adapter delegates to UiController.subscribeViewport; throws when the active binding has no hook. Subscribe call emits one `ux.response.ui.viewport.subscribe` acknowledgement; per-tick ViewportState deliveries flow through the listener (rAF-coalesced, U7).",
|
|
87
88
|
};
|
|
88
89
|
|
|
90
|
+
export const getPageResidencyMetadata: ApiV3FnMetadata = {
|
|
91
|
+
name: "ui.viewport.getPageResidency",
|
|
92
|
+
status: "live-with-adapter",
|
|
93
|
+
sourceLayer: "presentation",
|
|
94
|
+
liveEvidence: {
|
|
95
|
+
runnerTest: "test/api/v3/ui/viewport-residency.test.ts",
|
|
96
|
+
commit: "refactor-10-pe2-residency",
|
|
97
|
+
},
|
|
98
|
+
mockShape: {
|
|
99
|
+
deterministic: true,
|
|
100
|
+
seededFrom: "fixed",
|
|
101
|
+
shapeDescription: "Mock PageResidencySnapshot with residency='evicted' and status='unavailable' when no mounted controller exposes L10 residency policy.",
|
|
102
|
+
carriesMockFlag: true,
|
|
103
|
+
},
|
|
104
|
+
uxIntent: { uiVisible: false },
|
|
105
|
+
agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "ui-viewport-read" },
|
|
106
|
+
stateClass: "C-local",
|
|
107
|
+
persistsTo: "none",
|
|
108
|
+
rwdReference: "§PE2 § Virtual Page Windowing. Reads L10 page residency policy (realized/cold/evicted) without probing L05 geometry or realizing L11 DOM.",
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const subscribePageResidencyMetadata: ApiV3FnMetadata = {
|
|
112
|
+
name: "ui.viewport.subscribePageResidency",
|
|
113
|
+
status: "live-with-adapter",
|
|
114
|
+
sourceLayer: "presentation",
|
|
115
|
+
liveEvidence: {
|
|
116
|
+
runnerTest: "test/api/v3/ui/viewport-residency.test.ts",
|
|
117
|
+
commit: "refactor-10-pe2-residency",
|
|
118
|
+
},
|
|
119
|
+
uxIntent: {
|
|
120
|
+
uiVisible: true,
|
|
121
|
+
expectsUxResponse: "surface-refresh",
|
|
122
|
+
expectedDelta: "page-residency subscriber attached; future realized/cold/evicted changes propagate through the listener",
|
|
123
|
+
},
|
|
124
|
+
agentMetadata: { readOrMutate: "read", boundedScope: "session", auditCategory: "ui-viewport-subscribe" },
|
|
125
|
+
stateClass: "C-local",
|
|
126
|
+
persistsTo: "none",
|
|
127
|
+
bidirectional: true,
|
|
128
|
+
subscriptionShape: {
|
|
129
|
+
eventType: "ui.viewport.page_residency_changed",
|
|
130
|
+
payloadType: "PageResidencySnapshot",
|
|
131
|
+
coalescing: "raf",
|
|
132
|
+
},
|
|
133
|
+
rwdReference: "§PE2 § Virtual Page Windowing. Adapter delegates to UiController.subscribePageResidency; emissions are plain snapshots and do not dispatch PM transactions or force geometry rehydration.",
|
|
134
|
+
};
|
|
135
|
+
|
|
89
136
|
// ----- scrollToPage (coord-10 §γ — visual-fidelity / Go-to-page UX) -----
|
|
90
137
|
|
|
91
138
|
export const scrollToPageMetadata: ApiV3FnMetadata = {
|
|
@@ -245,6 +292,23 @@ export function createViewportFamily(ctx: UiApiContext) {
|
|
|
245
292
|
notifyMarkupModeSubscribers();
|
|
246
293
|
});
|
|
247
294
|
|
|
295
|
+
function normalizePageIndex(pageIndex: number): number {
|
|
296
|
+
if (!Number.isFinite(pageIndex)) return 0;
|
|
297
|
+
return Math.max(0, Math.floor(pageIndex));
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function unavailableResidency(pageIndex: number): PageResidencySnapshot {
|
|
301
|
+
return Object.freeze({
|
|
302
|
+
__mock: true,
|
|
303
|
+
pageIndex: normalizePageIndex(pageIndex),
|
|
304
|
+
residency: "evicted",
|
|
305
|
+
status: "unavailable",
|
|
306
|
+
revision: 0,
|
|
307
|
+
source: "unavailable",
|
|
308
|
+
reason: "page-residency policy is not wired on the active controller",
|
|
309
|
+
} as const);
|
|
310
|
+
}
|
|
311
|
+
|
|
248
312
|
return {
|
|
249
313
|
// Slice 11 — composes scroll/dpr/zoom from `handle.geometry` with
|
|
250
314
|
// width/height from the bound controller (see
|
|
@@ -279,6 +343,39 @@ export function createViewportFamily(ctx: UiApiContext) {
|
|
|
279
343
|
});
|
|
280
344
|
return unsubscribe;
|
|
281
345
|
},
|
|
346
|
+
getPageResidency(pageIndex: number): PageResidencySnapshot {
|
|
347
|
+
const normalized = normalizePageIndex(pageIndex);
|
|
348
|
+
const resolver = ctx.binding?.controller.getPageResidency;
|
|
349
|
+
if (!resolver) return unavailableResidency(normalized);
|
|
350
|
+
return resolver(normalized);
|
|
351
|
+
},
|
|
352
|
+
subscribePageResidency(
|
|
353
|
+
pageIndex: number,
|
|
354
|
+
listener: UiListener<PageResidencySnapshot>,
|
|
355
|
+
): UiUnsubscribe {
|
|
356
|
+
const controller = ctx.binding?.controller;
|
|
357
|
+
if (!controller) {
|
|
358
|
+
throw new Error(
|
|
359
|
+
"ui.viewport.subscribePageResidency: no controller bound — call ui.session.bind(controller) first",
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
if (!controller.subscribePageResidency) {
|
|
363
|
+
throw new Error(
|
|
364
|
+
`ui.viewport.subscribePageResidency: controller of kind "${controller.kind}" did not provide a subscribePageResidency hook`,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
const normalized = normalizePageIndex(pageIndex);
|
|
368
|
+
const unsubscribe = controller.subscribePageResidency(normalized, listener);
|
|
369
|
+
emitUxResponse(ctx.handle, {
|
|
370
|
+
apiFn: subscribePageResidencyMetadata.name,
|
|
371
|
+
intent: subscribePageResidencyMetadata.uxIntent.expectedDelta ?? "",
|
|
372
|
+
mockOrLive: "live-with-adapter",
|
|
373
|
+
uiVisible: true,
|
|
374
|
+
expectedDelta: subscribePageResidencyMetadata.uxIntent.expectedDelta,
|
|
375
|
+
actualDelta: { kind: "surface-refresh", payload: { subscribed: "ui.viewport.pageResidency", pageIndex: normalized } },
|
|
376
|
+
});
|
|
377
|
+
return unsubscribe;
|
|
378
|
+
},
|
|
282
379
|
|
|
283
380
|
// ----- scrollToPage (coord-10 §γ) -----
|
|
284
381
|
|
|
@@ -202,6 +202,27 @@ export interface RuntimeTableVerticalMergeCarry {
|
|
|
202
202
|
restartRowIndex: number;
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
export type RuntimeFragmentLayoutObjectKind =
|
|
206
|
+
| "paragraph"
|
|
207
|
+
| "numbered-paragraph"
|
|
208
|
+
| "table"
|
|
209
|
+
| "field-region"
|
|
210
|
+
| "footnote-body"
|
|
211
|
+
| "sdt-block"
|
|
212
|
+
| "opaque-block";
|
|
213
|
+
|
|
214
|
+
export interface RuntimeFragmentLayoutObject {
|
|
215
|
+
objectId: string;
|
|
216
|
+
kind: RuntimeFragmentLayoutObjectKind;
|
|
217
|
+
sourceBlockId: string;
|
|
218
|
+
paginationRole: "whole" | "slice" | "continuation";
|
|
219
|
+
measuredExtentTwips: {
|
|
220
|
+
heightTwips: number;
|
|
221
|
+
widthTwips?: number;
|
|
222
|
+
};
|
|
223
|
+
fieldFamilies?: readonly string[];
|
|
224
|
+
}
|
|
225
|
+
|
|
205
226
|
// ---------------------------------------------------------------------------
|
|
206
227
|
// Fragment types
|
|
207
228
|
// ---------------------------------------------------------------------------
|
|
@@ -265,6 +286,14 @@ export interface RuntimeBlockFragment {
|
|
|
265
286
|
* consumers without reading DOM or canonical tables again.
|
|
266
287
|
*/
|
|
267
288
|
continuation?: RuntimeLayoutContinuationCursor;
|
|
289
|
+
/**
|
|
290
|
+
* PE2 Slice 3 — measured layout-object descriptor for this fragment.
|
|
291
|
+
* This is the internal object protocol layer: one stable row per
|
|
292
|
+
* page-local fragment, expressed only in ids/enums/twips. It lets debug,
|
|
293
|
+
* render, and future truth joins reason about paragraph/table/note
|
|
294
|
+
* placement without re-reading DOM or source model objects.
|
|
295
|
+
*/
|
|
296
|
+
layoutObject?: RuntimeFragmentLayoutObject;
|
|
268
297
|
/**
|
|
269
298
|
* Slice 5 — opaque style-chain ref derived from the block's `styleId`.
|
|
270
299
|
* Used by `analyzeStylesChange` to bound invalidation to the first page
|
|
@@ -3271,14 +3271,6 @@ export function createDocumentRuntime(
|
|
|
3271
3271
|
const timestamp = clock();
|
|
3272
3272
|
|
|
3273
3273
|
if (suggesting) {
|
|
3274
|
-
if (activeStory.kind !== "main") {
|
|
3275
|
-
this.emitBlockedCommand(commandName, [{
|
|
3276
|
-
code: "suggesting_unsupported",
|
|
3277
|
-
message: `"${commandName}" is not supported in suggesting mode for this story.`,
|
|
3278
|
-
}]);
|
|
3279
|
-
return;
|
|
3280
|
-
}
|
|
3281
|
-
|
|
3282
3274
|
if (
|
|
3283
3275
|
operation.type === "set-alignment" ||
|
|
3284
3276
|
operation.type === "indent" ||
|
|
@@ -3313,9 +3305,14 @@ export function createDocumentRuntime(
|
|
|
3313
3305
|
},
|
|
3314
3306
|
timestamp,
|
|
3315
3307
|
);
|
|
3308
|
+
const nextFullDocument = stitchActiveStoryDocument(
|
|
3309
|
+
persistedDocument,
|
|
3310
|
+
activeStory,
|
|
3311
|
+
nextDocument,
|
|
3312
|
+
);
|
|
3316
3313
|
this.dispatch({
|
|
3317
3314
|
type: "document.replace",
|
|
3318
|
-
document: { ...
|
|
3315
|
+
document: { ...nextFullDocument, updatedAt: timestamp },
|
|
3319
3316
|
mapping: createEmptyMapping(),
|
|
3320
3317
|
selection: toInternalSelectionSnapshot(result.selection),
|
|
3321
3318
|
origin: createOrigin("api", timestamp),
|
|
@@ -3353,9 +3350,14 @@ export function createDocumentRuntime(
|
|
|
3353
3350
|
},
|
|
3354
3351
|
timestamp,
|
|
3355
3352
|
);
|
|
3353
|
+
const nextFullDocument = stitchActiveStoryDocument(
|
|
3354
|
+
persistedDocument,
|
|
3355
|
+
activeStory,
|
|
3356
|
+
nextDocument,
|
|
3357
|
+
);
|
|
3356
3358
|
this.dispatch({
|
|
3357
3359
|
type: "document.replace",
|
|
3358
|
-
document: { ...
|
|
3360
|
+
document: { ...nextFullDocument, updatedAt: timestamp },
|
|
3359
3361
|
mapping: createEmptyMapping(),
|
|
3360
3362
|
selection: toInternalSelectionSnapshot(result.selection),
|
|
3361
3363
|
origin: createOrigin("api", timestamp),
|
|
@@ -4187,13 +4189,6 @@ export function createDocumentRuntime(
|
|
|
4187
4189
|
const suggesting =
|
|
4188
4190
|
workflowCoordinator.getEffectiveDocumentMode(state.selection) === "suggesting";
|
|
4189
4191
|
if (suggesting) {
|
|
4190
|
-
if (activeStory.kind !== "main") {
|
|
4191
|
-
this.emitBlockedCommand("clearHighlight", [{
|
|
4192
|
-
code: "suggesting_unsupported",
|
|
4193
|
-
message: `"clearHighlight" is not supported in suggesting mode for this story.`,
|
|
4194
|
-
}]);
|
|
4195
|
-
return;
|
|
4196
|
-
}
|
|
4197
4192
|
const segment = findSingleSelectedTextSegment(syntheticSnapshot);
|
|
4198
4193
|
if (!segment) {
|
|
4199
4194
|
this.emitBlockedCommand("clearHighlight", [{
|
|
@@ -4225,9 +4220,14 @@ export function createDocumentRuntime(
|
|
|
4225
4220
|
},
|
|
4226
4221
|
timestamp,
|
|
4227
4222
|
);
|
|
4223
|
+
const nextFullDocument = stitchActiveStoryDocument(
|
|
4224
|
+
state.document,
|
|
4225
|
+
activeStory,
|
|
4226
|
+
nextDocument,
|
|
4227
|
+
);
|
|
4228
4228
|
this.dispatch({
|
|
4229
4229
|
type: "document.replace",
|
|
4230
|
-
document:
|
|
4230
|
+
document: { ...nextFullDocument, updatedAt: timestamp },
|
|
4231
4231
|
mapping: createEmptyMapping(),
|
|
4232
4232
|
origin: createOrigin("api", timestamp),
|
|
4233
4233
|
});
|
|
@@ -9223,6 +9223,27 @@ function appendPropertyChangeSuggestion(
|
|
|
9223
9223
|
};
|
|
9224
9224
|
}
|
|
9225
9225
|
|
|
9226
|
+
function stitchActiveStoryDocument(
|
|
9227
|
+
persistedDocument: CanonicalDocumentEnvelope,
|
|
9228
|
+
activeStory: EditorStoryTarget,
|
|
9229
|
+
storyDocument: CanonicalDocumentEnvelope,
|
|
9230
|
+
): CanonicalDocumentEnvelope {
|
|
9231
|
+
if (activeStory.kind === "main") {
|
|
9232
|
+
return storyDocument;
|
|
9233
|
+
}
|
|
9234
|
+
|
|
9235
|
+
return replaceStoryBlocks(
|
|
9236
|
+
{
|
|
9237
|
+
...persistedDocument,
|
|
9238
|
+
review: storyDocument.review,
|
|
9239
|
+
diagnostics: storyDocument.diagnostics,
|
|
9240
|
+
preservation: storyDocument.preservation,
|
|
9241
|
+
},
|
|
9242
|
+
activeStory,
|
|
9243
|
+
storyDocument.content.children,
|
|
9244
|
+
);
|
|
9245
|
+
}
|
|
9246
|
+
|
|
9226
9247
|
function createRuntimeSuggestionChangeId(
|
|
9227
9248
|
existing: CanonicalDocumentEnvelope["review"]["revisions"],
|
|
9228
9249
|
timestamp: string,
|
|
@@ -259,6 +259,11 @@ export function projectGeometryIndexFromFrame(
|
|
|
259
259
|
regionIds,
|
|
260
260
|
});
|
|
261
261
|
recordPrecision(precision, "exact");
|
|
262
|
+
appendPageLocalObjectHandleEntries({
|
|
263
|
+
page,
|
|
264
|
+
entries: objectHandleEntries,
|
|
265
|
+
precision,
|
|
266
|
+
});
|
|
262
267
|
}
|
|
263
268
|
|
|
264
269
|
appendScopeReplacementEnvelopeEntries({
|
|
@@ -762,6 +767,75 @@ function appendCanonicalObjectHandleEntries(input: {
|
|
|
762
767
|
}
|
|
763
768
|
}
|
|
764
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",
|
|
795
|
+
joinKind: "block-scoped",
|
|
796
|
+
};
|
|
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
|
+
|
|
765
839
|
function finalizeObjectHandleEntries(
|
|
766
840
|
entries: ReadonlyMap<string, MutableObjectHandleEntry>,
|
|
767
841
|
): GeometryObjectHandleEntry[] {
|
|
@@ -1074,8 +1074,15 @@
|
|
|
1074
1074
|
* table header rows, and vertical-merge carry metadata. Cache envelopes
|
|
1075
1075
|
* from v67 invalidate because fragment payloads can now include
|
|
1076
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`.
|
|
1077
1084
|
*/
|
|
1078
|
-
export const LAYOUT_ENGINE_VERSION =
|
|
1085
|
+
export const LAYOUT_ENGINE_VERSION = 69 as const;
|
|
1079
1086
|
|
|
1080
1087
|
/**
|
|
1081
1088
|
* Serialization schema version for the LayCache payload (the cache envelope
|
|
@@ -72,6 +72,8 @@ export type {
|
|
|
72
72
|
RuntimeParagraphContinuationCursor,
|
|
73
73
|
RuntimeTableContinuationCursor,
|
|
74
74
|
RuntimeTableVerticalMergeCarry,
|
|
75
|
+
RuntimeFragmentLayoutObjectKind,
|
|
76
|
+
RuntimeFragmentLayoutObject,
|
|
75
77
|
RuntimeBlockFragment,
|
|
76
78
|
RuntimeLineBox,
|
|
77
79
|
RuntimeNoteAllocation,
|
|
@@ -1791,6 +1791,16 @@ export function paginateSectionBlocksWithSplits(
|
|
|
1791
1791
|
to: refRange.blockTo,
|
|
1792
1792
|
heightTwips: heightTwips + FOOTNOTE_REFERENCE_RESERVATION_TWIPS,
|
|
1793
1793
|
kind: "whole",
|
|
1794
|
+
layoutObject: {
|
|
1795
|
+
objectId: `footnote-body:${fragmentId}`,
|
|
1796
|
+
kind: "footnote-body",
|
|
1797
|
+
sourceBlockId: `note-body-${noteKind}-${noteId}`,
|
|
1798
|
+
paginationRole: "whole",
|
|
1799
|
+
measuredExtentTwips: {
|
|
1800
|
+
heightTwips: heightTwips + FOOTNOTE_REFERENCE_RESERVATION_TWIPS,
|
|
1801
|
+
widthTwips: effectiveColumnWidth,
|
|
1802
|
+
},
|
|
1803
|
+
},
|
|
1794
1804
|
};
|
|
1795
1805
|
|
|
1796
1806
|
allocations.push(allocation);
|
|
@@ -126,6 +126,14 @@ export function projectSurfaceBlocksToPageFragments(
|
|
|
126
126
|
if (pageIndex === null) continue;
|
|
127
127
|
|
|
128
128
|
const columnIndex = columnIndexFor(pageIndex, block.blockId);
|
|
129
|
+
const heightTwips = measuredHeightFor(
|
|
130
|
+
pageIndex,
|
|
131
|
+
block.blockId,
|
|
132
|
+
estimateBlockHeightFromSpan(block),
|
|
133
|
+
);
|
|
134
|
+
const widthTwips = fragmentMeasurementsByPageIndex
|
|
135
|
+
?.get(pageIndex)
|
|
136
|
+
?.get(block.blockId)?.widthTwips;
|
|
129
137
|
const fragment: FragmentWithoutPageId = {
|
|
130
138
|
fragmentId: `fragment-${block.blockId}`,
|
|
131
139
|
blockId: block.blockId,
|
|
@@ -133,13 +141,16 @@ export function projectSurfaceBlocksToPageFragments(
|
|
|
133
141
|
regionKind: "body",
|
|
134
142
|
from: block.from,
|
|
135
143
|
to: block.to,
|
|
136
|
-
heightTwips
|
|
137
|
-
pageIndex,
|
|
138
|
-
block.blockId,
|
|
139
|
-
estimateBlockHeightFromSpan(block),
|
|
140
|
-
),
|
|
144
|
+
heightTwips,
|
|
141
145
|
...deriveStyleMetadata(block),
|
|
142
146
|
kind: "whole",
|
|
147
|
+
layoutObject: buildFragmentLayoutObject({
|
|
148
|
+
block,
|
|
149
|
+
fragmentId: `fragment-${block.blockId}`,
|
|
150
|
+
heightTwips,
|
|
151
|
+
widthTwips,
|
|
152
|
+
paginationRole: "whole",
|
|
153
|
+
}),
|
|
143
154
|
...(columnIndex !== undefined ? { columnIndex } : {}),
|
|
144
155
|
};
|
|
145
156
|
|
|
@@ -224,6 +235,7 @@ function emitSlicedParagraph(
|
|
|
224
235
|
): void {
|
|
225
236
|
for (let i = 0; i < slices.length; i += 1) {
|
|
226
237
|
const slice = slices[i]!;
|
|
238
|
+
const heightTwips = slice.heightTwips ?? estimateSliceHeightFromLines(slice.lineRange);
|
|
227
239
|
const fragment: FragmentWithoutPageId = {
|
|
228
240
|
fragmentId: `fragment-${block.blockId}-slice-${i}`,
|
|
229
241
|
blockId: block.blockId,
|
|
@@ -231,11 +243,18 @@ function emitSlicedParagraph(
|
|
|
231
243
|
regionKind: "body",
|
|
232
244
|
from: block.from,
|
|
233
245
|
to: block.to,
|
|
234
|
-
heightTwips
|
|
246
|
+
heightTwips,
|
|
235
247
|
...deriveStyleMetadata(block),
|
|
236
248
|
kind: "paragraph-slice",
|
|
237
249
|
paragraphLineRange: slice.lineRange,
|
|
238
250
|
continuation: buildParagraphContinuationCursor(slice, i, slices.length),
|
|
251
|
+
layoutObject: buildFragmentLayoutObject({
|
|
252
|
+
block,
|
|
253
|
+
fragmentId: `fragment-${block.blockId}-slice-${i}`,
|
|
254
|
+
heightTwips,
|
|
255
|
+
widthTwips: slice.widthTwips,
|
|
256
|
+
paginationRole: slice.lineRange.from > 0 ? "continuation" : "slice",
|
|
257
|
+
}),
|
|
239
258
|
};
|
|
240
259
|
emit(slice.pageIndex, fragment);
|
|
241
260
|
}
|
|
@@ -281,6 +300,7 @@ function emitSlicedTable(
|
|
|
281
300
|
): void {
|
|
282
301
|
for (let i = 0; i < slices.length; i += 1) {
|
|
283
302
|
const slice = slices[i]!;
|
|
303
|
+
const heightTwips = slice.heightTwips ?? estimateSliceHeightFromRows(slice.rowRange);
|
|
284
304
|
const fragment: FragmentWithoutPageId = {
|
|
285
305
|
fragmentId: `fragment-${block.blockId}-rowslice-${i}`,
|
|
286
306
|
blockId: block.blockId,
|
|
@@ -288,11 +308,17 @@ function emitSlicedTable(
|
|
|
288
308
|
regionKind: "body",
|
|
289
309
|
from: block.from,
|
|
290
310
|
to: block.to,
|
|
291
|
-
heightTwips
|
|
311
|
+
heightTwips,
|
|
292
312
|
...deriveStyleMetadata(block),
|
|
293
313
|
kind: "table-slice",
|
|
294
314
|
tableRowRange: slice.rowRange,
|
|
295
315
|
continuation: buildTableContinuationCursor(block, slice, i, slices.length),
|
|
316
|
+
layoutObject: buildFragmentLayoutObject({
|
|
317
|
+
block,
|
|
318
|
+
fragmentId: `fragment-${block.blockId}-rowslice-${i}`,
|
|
319
|
+
heightTwips,
|
|
320
|
+
paginationRole: slice.rowRange.from > 0 ? "continuation" : "slice",
|
|
321
|
+
}),
|
|
296
322
|
...(slice.columnIndex !== undefined ? { columnIndex: slice.columnIndex } : {}),
|
|
297
323
|
};
|
|
298
324
|
emit(slice.pageIndex, fragment);
|
|
@@ -391,6 +417,59 @@ function estimateSliceHeightFromRows(rowRange: {
|
|
|
391
417
|
return rows * 360; // ~1 line + padding per row, approximate
|
|
392
418
|
}
|
|
393
419
|
|
|
420
|
+
function buildFragmentLayoutObject(input: {
|
|
421
|
+
block: SurfaceBlockSnapshot;
|
|
422
|
+
fragmentId: string;
|
|
423
|
+
heightTwips: number;
|
|
424
|
+
widthTwips?: number;
|
|
425
|
+
paginationRole: NonNullable<RuntimeBlockFragment["layoutObject"]>["paginationRole"];
|
|
426
|
+
}): NonNullable<RuntimeBlockFragment["layoutObject"]> {
|
|
427
|
+
const fieldFamilies = collectFieldFamilies(input.block);
|
|
428
|
+
const kind = resolveFragmentLayoutObjectKind(input.block, fieldFamilies);
|
|
429
|
+
return {
|
|
430
|
+
objectId: `${kind}:${input.fragmentId}`,
|
|
431
|
+
kind,
|
|
432
|
+
sourceBlockId: input.block.blockId,
|
|
433
|
+
paginationRole: input.paginationRole,
|
|
434
|
+
measuredExtentTwips: {
|
|
435
|
+
heightTwips: Math.max(0, input.heightTwips),
|
|
436
|
+
...(input.widthTwips !== undefined
|
|
437
|
+
? { widthTwips: Math.max(0, input.widthTwips) }
|
|
438
|
+
: {}),
|
|
439
|
+
},
|
|
440
|
+
...(fieldFamilies.length > 0 ? { fieldFamilies } : {}),
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function resolveFragmentLayoutObjectKind(
|
|
445
|
+
block: SurfaceBlockSnapshot,
|
|
446
|
+
fieldFamilies: readonly string[],
|
|
447
|
+
): NonNullable<RuntimeBlockFragment["layoutObject"]>["kind"] {
|
|
448
|
+
switch (block.kind) {
|
|
449
|
+
case "paragraph":
|
|
450
|
+
if (fieldFamilies.length > 0) return "field-region";
|
|
451
|
+
return block.numbering ? "numbered-paragraph" : "paragraph";
|
|
452
|
+
case "table":
|
|
453
|
+
return "table";
|
|
454
|
+
case "sdt_block":
|
|
455
|
+
return "sdt-block";
|
|
456
|
+
case "opaque_block":
|
|
457
|
+
return "opaque-block";
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function collectFieldFamilies(block: SurfaceBlockSnapshot): string[] {
|
|
462
|
+
if (block.kind !== "paragraph") return [];
|
|
463
|
+
const families: string[] = [];
|
|
464
|
+
const seen = new Set<string>();
|
|
465
|
+
for (const segment of block.segments) {
|
|
466
|
+
if (segment.kind !== "field_ref" || seen.has(segment.fieldFamily)) continue;
|
|
467
|
+
seen.add(segment.fieldFamily);
|
|
468
|
+
families.push(segment.fieldFamily);
|
|
469
|
+
}
|
|
470
|
+
return families;
|
|
471
|
+
}
|
|
472
|
+
|
|
394
473
|
function findPageIndexForOffset(
|
|
395
474
|
pages: readonly DocumentPageSnapshot[],
|
|
396
475
|
offset: number,
|