@embedpdf/plugin-annotation 2.14.4 → 3.0.0-next.0
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/LICENSE +192 -21
- package/dist/capability-fghl0JmE.js +2680 -0
- package/dist/capability-fghl0JmE.js.map +1 -0
- package/dist/capability-hb2AU0f6.cjs +2739 -0
- package/dist/capability-hb2AU0f6.cjs.map +1 -0
- package/dist/index.cjs +593 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +65 -0
- package/dist/index.d.ts +65 -1
- package/dist/index.js +562 -6998
- package/dist/index.js.map +1 -1
- package/dist/internal.cjs +5 -0
- package/dist/internal.d.cts +13 -0
- package/dist/internal.d.ts +13 -0
- package/dist/internal.js +2 -0
- package/dist/types-BhWgqgbX.d.cts +879 -0
- package/dist/types-Da6JRmhw.d.ts +879 -0
- package/package.json +26 -66
- package/dist/lib/actions.d.ts +0 -192
- package/dist/lib/annotation-plugin.d.ts +0 -310
- package/dist/lib/geometry/cloudy-border.d.ts +0 -90
- package/dist/lib/geometry/index.d.ts +0 -2
- package/dist/lib/geometry/rotation.d.ts +0 -32
- package/dist/lib/handlers/callout-free-text.handler.d.ts +0 -3
- package/dist/lib/handlers/circle.handler.d.ts +0 -3
- package/dist/lib/handlers/click-detector.d.ts +0 -27
- package/dist/lib/handlers/free-text.handler.d.ts +0 -3
- package/dist/lib/handlers/index.d.ts +0 -14
- package/dist/lib/handlers/ink.handler.d.ts +0 -3
- package/dist/lib/handlers/insert-text.handler.d.ts +0 -8
- package/dist/lib/handlers/line.handler.d.ts +0 -3
- package/dist/lib/handlers/link.handler.d.ts +0 -3
- package/dist/lib/handlers/polygon.handler.d.ts +0 -3
- package/dist/lib/handlers/polyline.handler.d.ts +0 -3
- package/dist/lib/handlers/replace-text.handler.d.ts +0 -9
- package/dist/lib/handlers/selection-utils.d.ts +0 -7
- package/dist/lib/handlers/square.handler.d.ts +0 -3
- package/dist/lib/handlers/stamp.handler.d.ts +0 -3
- package/dist/lib/handlers/text-markup.handler.d.ts +0 -7
- package/dist/lib/handlers/text.handler.d.ts +0 -3
- package/dist/lib/handlers/types.d.ts +0 -176
- package/dist/lib/helpers.d.ts +0 -46
- package/dist/lib/index.d.ts +0 -21
- package/dist/lib/manifest.d.ts +0 -4
- package/dist/lib/patching/base-patch.d.ts +0 -87
- package/dist/lib/patching/index.d.ts +0 -6
- package/dist/lib/patching/insert-upright.d.ts +0 -20
- package/dist/lib/patching/line-ending-handlers.d.ts +0 -20
- package/dist/lib/patching/line-endings.d.ts +0 -13
- package/dist/lib/patching/patch-registry.d.ts +0 -40
- package/dist/lib/patching/patch-utils.d.ts +0 -97
- package/dist/lib/patching/patches/callout-freetext.patch.d.ts +0 -3
- package/dist/lib/patching/patches/circle.patch.d.ts +0 -3
- package/dist/lib/patching/patches/freetext.patch.d.ts +0 -3
- package/dist/lib/patching/patches/index.d.ts +0 -9
- package/dist/lib/patching/patches/ink.patch.d.ts +0 -3
- package/dist/lib/patching/patches/line.patch.d.ts +0 -3
- package/dist/lib/patching/patches/polygon.patch.d.ts +0 -3
- package/dist/lib/patching/patches/polyline.patch.d.ts +0 -3
- package/dist/lib/patching/patches/square.patch.d.ts +0 -3
- package/dist/lib/patching/patches/stamp.patch.d.ts +0 -3
- package/dist/lib/reducer.d.ts +0 -6
- package/dist/lib/selectors.d.ts +0 -139
- package/dist/lib/tools/default-tools.d.ts +0 -521
- package/dist/lib/tools/tools-utils.d.ts +0 -453
- package/dist/lib/tools/types.d.ts +0 -209
- package/dist/lib/types.d.ts +0 -671
- package/dist/lib/utils/index.d.ts +0 -1
- package/dist/lib/utils/use-state.d.ts +0 -12
- package/dist/preact/adapter.d.ts +0 -14
- package/dist/preact/core.d.ts +0 -1
- package/dist/preact/index.cjs +0 -2
- package/dist/preact/index.cjs.map +0 -1
- package/dist/preact/index.d.ts +0 -1
- package/dist/preact/index.js +0 -4013
- package/dist/preact/index.js.map +0 -1
- package/dist/preact/interaction-manager.d.ts +0 -1
- package/dist/preact/selection.d.ts +0 -1
- package/dist/preact/utils.d.ts +0 -1
- package/dist/react/adapter.d.ts +0 -6
- package/dist/react/core.d.ts +0 -1
- package/dist/react/index.cjs +0 -2
- package/dist/react/index.cjs.map +0 -1
- package/dist/react/index.d.ts +0 -1
- package/dist/react/index.js +0 -4014
- package/dist/react/index.js.map +0 -1
- package/dist/react/interaction-manager.d.ts +0 -1
- package/dist/react/selection.d.ts +0 -1
- package/dist/react/utils.d.ts +0 -1
- package/dist/shared/annotation-bounds.d.ts +0 -14
- package/dist/shared/components/annotation-container.d.ts +0 -71
- package/dist/shared/components/annotation-layer.d.ts +0 -33
- package/dist/shared/components/annotation-navigation-handler.d.ts +0 -1
- package/dist/shared/components/annotation-paint-layer.d.ts +0 -7
- package/dist/shared/components/annotations/callout-free-text-preview.d.ts +0 -15
- package/dist/shared/components/annotations/callout-free-text.d.ts +0 -15
- package/dist/shared/components/annotations/caret.d.ts +0 -24
- package/dist/shared/components/annotations/circle.d.ts +0 -35
- package/dist/shared/components/annotations/free-text.d.ts +0 -17
- package/dist/shared/components/annotations/ink.d.ts +0 -27
- package/dist/shared/components/annotations/line.d.ts +0 -35
- package/dist/shared/components/annotations/link-locked.d.ts +0 -3
- package/dist/shared/components/annotations/link.d.ts +0 -28
- package/dist/shared/components/annotations/polygon.d.ts +0 -23
- package/dist/shared/components/annotations/polyline.d.ts +0 -23
- package/dist/shared/components/annotations/square.d.ts +0 -35
- package/dist/shared/components/annotations/stamp.d.ts +0 -13
- package/dist/shared/components/annotations/text.d.ts +0 -14
- package/dist/shared/components/annotations.d.ts +0 -22
- package/dist/shared/components/appearance-image.d.ts +0 -12
- package/dist/shared/components/built-in-renderers.d.ts +0 -2
- package/dist/shared/components/group-selection-box.d.ts +0 -40
- package/dist/shared/components/index.d.ts +0 -2
- package/dist/shared/components/preview-renderer.d.ts +0 -8
- package/dist/shared/components/render-annotation.d.ts +0 -13
- package/dist/shared/components/text-markup/highlight.d.ts +0 -16
- package/dist/shared/components/text-markup/squiggly.d.ts +0 -16
- package/dist/shared/components/text-markup/strikeout.d.ts +0 -16
- package/dist/shared/components/text-markup/underline.d.ts +0 -16
- package/dist/shared/components/text-markup.d.ts +0 -7
- package/dist/shared/components/types.d.ts +0 -253
- package/dist/shared/context/index.d.ts +0 -1
- package/dist/shared/context/renderer-registry.d.ts +0 -21
- package/dist/shared/hooks/index.d.ts +0 -2
- package/dist/shared/hooks/use-annotation.d.ts +0 -19
- package/dist/shared/hooks/use-ios-zoom-prevention.d.ts +0 -16
- package/dist/shared/index.d.ts +0 -6
- package/dist/shared/types.d.ts +0 -10
- package/dist/shared-preact/annotation-bounds.d.ts +0 -14
- package/dist/shared-preact/components/annotation-container.d.ts +0 -71
- package/dist/shared-preact/components/annotation-layer.d.ts +0 -33
- package/dist/shared-preact/components/annotation-navigation-handler.d.ts +0 -1
- package/dist/shared-preact/components/annotation-paint-layer.d.ts +0 -7
- package/dist/shared-preact/components/annotations/callout-free-text-preview.d.ts +0 -15
- package/dist/shared-preact/components/annotations/callout-free-text.d.ts +0 -15
- package/dist/shared-preact/components/annotations/caret.d.ts +0 -24
- package/dist/shared-preact/components/annotations/circle.d.ts +0 -35
- package/dist/shared-preact/components/annotations/free-text.d.ts +0 -17
- package/dist/shared-preact/components/annotations/ink.d.ts +0 -27
- package/dist/shared-preact/components/annotations/line.d.ts +0 -35
- package/dist/shared-preact/components/annotations/link-locked.d.ts +0 -3
- package/dist/shared-preact/components/annotations/link.d.ts +0 -28
- package/dist/shared-preact/components/annotations/polygon.d.ts +0 -23
- package/dist/shared-preact/components/annotations/polyline.d.ts +0 -23
- package/dist/shared-preact/components/annotations/square.d.ts +0 -35
- package/dist/shared-preact/components/annotations/stamp.d.ts +0 -13
- package/dist/shared-preact/components/annotations/text.d.ts +0 -14
- package/dist/shared-preact/components/annotations.d.ts +0 -22
- package/dist/shared-preact/components/appearance-image.d.ts +0 -12
- package/dist/shared-preact/components/built-in-renderers.d.ts +0 -2
- package/dist/shared-preact/components/group-selection-box.d.ts +0 -40
- package/dist/shared-preact/components/index.d.ts +0 -2
- package/dist/shared-preact/components/preview-renderer.d.ts +0 -8
- package/dist/shared-preact/components/render-annotation.d.ts +0 -13
- package/dist/shared-preact/components/text-markup/highlight.d.ts +0 -16
- package/dist/shared-preact/components/text-markup/squiggly.d.ts +0 -16
- package/dist/shared-preact/components/text-markup/strikeout.d.ts +0 -16
- package/dist/shared-preact/components/text-markup/underline.d.ts +0 -16
- package/dist/shared-preact/components/text-markup.d.ts +0 -7
- package/dist/shared-preact/components/types.d.ts +0 -253
- package/dist/shared-preact/context/index.d.ts +0 -1
- package/dist/shared-preact/context/renderer-registry.d.ts +0 -21
- package/dist/shared-preact/hooks/index.d.ts +0 -2
- package/dist/shared-preact/hooks/use-annotation.d.ts +0 -19
- package/dist/shared-preact/hooks/use-ios-zoom-prevention.d.ts +0 -15
- package/dist/shared-preact/index.d.ts +0 -6
- package/dist/shared-preact/types.d.ts +0 -10
- package/dist/shared-react/annotation-bounds.d.ts +0 -14
- package/dist/shared-react/components/annotation-container.d.ts +0 -71
- package/dist/shared-react/components/annotation-layer.d.ts +0 -33
- package/dist/shared-react/components/annotation-navigation-handler.d.ts +0 -1
- package/dist/shared-react/components/annotation-paint-layer.d.ts +0 -7
- package/dist/shared-react/components/annotations/callout-free-text-preview.d.ts +0 -15
- package/dist/shared-react/components/annotations/callout-free-text.d.ts +0 -15
- package/dist/shared-react/components/annotations/caret.d.ts +0 -24
- package/dist/shared-react/components/annotations/circle.d.ts +0 -35
- package/dist/shared-react/components/annotations/free-text.d.ts +0 -17
- package/dist/shared-react/components/annotations/ink.d.ts +0 -27
- package/dist/shared-react/components/annotations/line.d.ts +0 -35
- package/dist/shared-react/components/annotations/link-locked.d.ts +0 -3
- package/dist/shared-react/components/annotations/link.d.ts +0 -28
- package/dist/shared-react/components/annotations/polygon.d.ts +0 -23
- package/dist/shared-react/components/annotations/polyline.d.ts +0 -23
- package/dist/shared-react/components/annotations/square.d.ts +0 -35
- package/dist/shared-react/components/annotations/stamp.d.ts +0 -13
- package/dist/shared-react/components/annotations/text.d.ts +0 -14
- package/dist/shared-react/components/annotations.d.ts +0 -22
- package/dist/shared-react/components/appearance-image.d.ts +0 -12
- package/dist/shared-react/components/built-in-renderers.d.ts +0 -2
- package/dist/shared-react/components/group-selection-box.d.ts +0 -40
- package/dist/shared-react/components/index.d.ts +0 -2
- package/dist/shared-react/components/preview-renderer.d.ts +0 -8
- package/dist/shared-react/components/render-annotation.d.ts +0 -13
- package/dist/shared-react/components/text-markup/highlight.d.ts +0 -16
- package/dist/shared-react/components/text-markup/squiggly.d.ts +0 -16
- package/dist/shared-react/components/text-markup/strikeout.d.ts +0 -16
- package/dist/shared-react/components/text-markup/underline.d.ts +0 -16
- package/dist/shared-react/components/text-markup.d.ts +0 -7
- package/dist/shared-react/components/types.d.ts +0 -253
- package/dist/shared-react/context/index.d.ts +0 -1
- package/dist/shared-react/context/renderer-registry.d.ts +0 -21
- package/dist/shared-react/hooks/index.d.ts +0 -2
- package/dist/shared-react/hooks/use-annotation.d.ts +0 -19
- package/dist/shared-react/hooks/use-ios-zoom-prevention.d.ts +0 -16
- package/dist/shared-react/index.d.ts +0 -6
- package/dist/shared-react/types.d.ts +0 -10
- package/dist/shared-vue/annotation-bounds.d.ts +0 -14
- package/dist/svelte/components/AnnotationContainer.svelte.d.ts +0 -26
- package/dist/svelte/components/AnnotationLayer.svelte.d.ts +0 -40
- package/dist/svelte/components/AnnotationNavigationHandler.svelte.d.ts +0 -3
- package/dist/svelte/components/AnnotationPaintLayer.svelte.d.ts +0 -8
- package/dist/svelte/components/Annotations.svelte.d.ts +0 -28
- package/dist/svelte/components/AppearanceImage.svelte.d.ts +0 -8
- package/dist/svelte/components/GroupSelectionBox.svelte.d.ts +0 -40
- package/dist/svelte/components/PreviewRenderer.svelte.d.ts +0 -9
- package/dist/svelte/components/RenderAnnotation.svelte.d.ts +0 -14
- package/dist/svelte/components/RendererRegistryProvider.svelte.d.ts +0 -7
- package/dist/svelte/components/TextMarkup.svelte.d.ts +0 -8
- package/dist/svelte/components/annotations/CalloutFreeText.svelte.d.ts +0 -15
- package/dist/svelte/components/annotations/CalloutFreeTextPreview.svelte.d.ts +0 -10
- package/dist/svelte/components/annotations/Caret.svelte.d.ts +0 -13
- package/dist/svelte/components/annotations/Circle.svelte.d.ts +0 -19
- package/dist/svelte/components/annotations/CirclePreview.svelte.d.ts +0 -10
- package/dist/svelte/components/annotations/FreeText.svelte.d.ts +0 -16
- package/dist/svelte/components/annotations/FreeTextPreview.svelte.d.ts +0 -10
- package/dist/svelte/components/annotations/Ink.svelte.d.ts +0 -15
- package/dist/svelte/components/annotations/InkPreview.svelte.d.ts +0 -10
- package/dist/svelte/components/annotations/Line.svelte.d.ts +0 -19
- package/dist/svelte/components/annotations/LinePreview.svelte.d.ts +0 -10
- package/dist/svelte/components/annotations/Link.svelte.d.ts +0 -24
- package/dist/svelte/components/annotations/LinkLockedMode.svelte.d.ts +0 -5
- package/dist/svelte/components/annotations/LinkPreview.svelte.d.ts +0 -10
- package/dist/svelte/components/annotations/Polygon.svelte.d.ts +0 -21
- package/dist/svelte/components/annotations/PolygonPreview.svelte.d.ts +0 -10
- package/dist/svelte/components/annotations/Polyline.svelte.d.ts +0 -19
- package/dist/svelte/components/annotations/PolylinePreview.svelte.d.ts +0 -10
- package/dist/svelte/components/annotations/Square.svelte.d.ts +0 -19
- package/dist/svelte/components/annotations/SquarePreview.svelte.d.ts +0 -10
- package/dist/svelte/components/annotations/Stamp.svelte.d.ts +0 -13
- package/dist/svelte/components/annotations/StampPreview.svelte.d.ts +0 -10
- package/dist/svelte/components/annotations/Text.svelte.d.ts +0 -10
- package/dist/svelte/components/annotations/index.d.ts +0 -10
- package/dist/svelte/components/built-in-renderers.d.ts +0 -2
- package/dist/svelte/components/index.d.ts +0 -11
- package/dist/svelte/components/renderers/CalloutFreeTextRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/CaretRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/CircleRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/FreeTextRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/HighlightRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/InkRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/LineRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/LinkRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/PolygonRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/PolylineRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/SquareRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/SquigglyRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/StampRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/StrikeoutRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/TextRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/renderers/UnderlineRenderer.svelte.d.ts +0 -5
- package/dist/svelte/components/text-markup/Highlight.svelte.d.ts +0 -16
- package/dist/svelte/components/text-markup/Squiggly.svelte.d.ts +0 -16
- package/dist/svelte/components/text-markup/Strikeout.svelte.d.ts +0 -16
- package/dist/svelte/components/text-markup/Underline.svelte.d.ts +0 -16
- package/dist/svelte/components/text-markup/index.d.ts +0 -4
- package/dist/svelte/components/types.d.ts +0 -66
- package/dist/svelte/context/index.d.ts +0 -2
- package/dist/svelte/context/renderer-registry.svelte.d.ts +0 -20
- package/dist/svelte/context/types.d.ts +0 -129
- package/dist/svelte/hooks/index.d.ts +0 -2
- package/dist/svelte/hooks/use-annotation.svelte.d.ts +0 -21
- package/dist/svelte/hooks/use-ios-zoom-prevention.svelte.d.ts +0 -6
- package/dist/svelte/index.cjs +0 -2
- package/dist/svelte/index.cjs.map +0 -1
- package/dist/svelte/index.d.ts +0 -6
- package/dist/svelte/index.js +0 -5237
- package/dist/svelte/index.js.map +0 -1
- package/dist/svelte/types.d.ts +0 -126
- package/dist/vue/components/annotation-container.vue.d.ts +0 -134
- package/dist/vue/components/annotation-layer.vue.d.ts +0 -99
- package/dist/vue/components/annotation-navigation-handler.vue.d.ts +0 -3
- package/dist/vue/components/annotation-paint-layer.vue.d.ts +0 -8
- package/dist/vue/components/annotations/callout-free-text-preview.vue.d.ts +0 -10
- package/dist/vue/components/annotations/callout-free-text.vue.d.ts +0 -25
- package/dist/vue/components/annotations/caret.vue.d.ts +0 -24
- package/dist/vue/components/annotations/circle-preview.vue.d.ts +0 -10
- package/dist/vue/components/annotations/circle.vue.d.ts +0 -37
- package/dist/vue/components/annotations/free-text-preview.vue.d.ts +0 -10
- package/dist/vue/components/annotations/free-text.vue.d.ts +0 -25
- package/dist/vue/components/annotations/index.d.ts +0 -10
- package/dist/vue/components/annotations/ink-preview.vue.d.ts +0 -10
- package/dist/vue/components/annotations/ink.vue.d.ts +0 -27
- package/dist/vue/components/annotations/line-preview.vue.d.ts +0 -10
- package/dist/vue/components/annotations/line.vue.d.ts +0 -38
- package/dist/vue/components/annotations/link-locked.vue.d.ts +0 -5
- package/dist/vue/components/annotations/link-preview.vue.d.ts +0 -10
- package/dist/vue/components/annotations/link.vue.d.ts +0 -47
- package/dist/vue/components/annotations/polygon-preview.vue.d.ts +0 -10
- package/dist/vue/components/annotations/polygon.vue.d.ts +0 -43
- package/dist/vue/components/annotations/polyline-preview.vue.d.ts +0 -10
- package/dist/vue/components/annotations/polyline.vue.d.ts +0 -38
- package/dist/vue/components/annotations/square-preview.vue.d.ts +0 -10
- package/dist/vue/components/annotations/square.vue.d.ts +0 -37
- package/dist/vue/components/annotations/stamp-preview.vue.d.ts +0 -10
- package/dist/vue/components/annotations/stamp.vue.d.ts +0 -19
- package/dist/vue/components/annotations/text.vue.d.ts +0 -14
- package/dist/vue/components/annotations.vue.d.ts +0 -111
- package/dist/vue/components/appearance-image.vue.d.ts +0 -9
- package/dist/vue/components/built-in-renderers.d.ts +0 -2
- package/dist/vue/components/group-selection-box.vue.d.ts +0 -98
- package/dist/vue/components/index.d.ts +0 -11
- package/dist/vue/components/preview-renderer.vue.d.ts +0 -9
- package/dist/vue/components/render-annotation.vue.d.ts +0 -16
- package/dist/vue/components/renderer-registry-provider.vue.d.ts +0 -13
- package/dist/vue/components/renderers/callout-free-text-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/caret-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/circle-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/free-text-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/highlight-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/ink-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/line-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/link-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/polygon-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/polyline-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/square-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/squiggly-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/stamp-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/strikeout-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/text-renderer.vue.d.ts +0 -6
- package/dist/vue/components/renderers/underline-renderer.vue.d.ts +0 -6
- package/dist/vue/components/text-markup/highlight.vue.d.ts +0 -27
- package/dist/vue/components/text-markup/index.d.ts +0 -4
- package/dist/vue/components/text-markup/squiggly.vue.d.ts +0 -27
- package/dist/vue/components/text-markup/strikeout.vue.d.ts +0 -27
- package/dist/vue/components/text-markup/underline.vue.d.ts +0 -27
- package/dist/vue/components/text-markup.vue.d.ts +0 -8
- package/dist/vue/context/index.d.ts +0 -2
- package/dist/vue/context/renderer-registry.d.ts +0 -26
- package/dist/vue/context/types.d.ts +0 -129
- package/dist/vue/hooks/index.d.ts +0 -2
- package/dist/vue/hooks/use-annotation.d.ts +0 -50
- package/dist/vue/hooks/use-ios-zoom-prevention.d.ts +0 -7
- package/dist/vue/index.cjs +0 -2
- package/dist/vue/index.cjs.map +0 -1
- package/dist/vue/index.d.ts +0 -6
- package/dist/vue/index.js +0 -4984
- package/dist/vue/index.js.map +0 -1
- package/dist/vue/types.d.ts +0 -94
|
@@ -0,0 +1,2680 @@
|
|
|
1
|
+
import { CONTINUOUS_RENDER_POLICY, createCapabilityToken, snapAppearanceScale } from "@embedpdf/core";
|
|
2
|
+
import { InteractionToken } from "@embedpdf/plugin-interaction";
|
|
3
|
+
import { resolveBinarySource, sniffBinaryMetadata } from "@embedpdf/engine-core/runtime";
|
|
4
|
+
import { FLAG_KEYS, calloutLinePoints, chrome, clickCreateGeom, cloudyBorderExtent, contentToPdfPoint, contentToPdfRect, contentToPdfRect as contentToPdfRect$1, creationDraftAnchor, cursorAt, defaultsFor, expandGroups, fitStampBox, geomPdfBounds, geomRotation, geomVisualBounds, groupKeyOf, hitTest, initialTextStyle, normalizeDeg, pageItems, pdfToContentPoint, pdfToContentPoint as pdfToContentPoint$1, pdfToContentRect, pdfToContentRect as pdfToContentRect$1, propsFor, readProp, resolveClickPlacement, rotatedAabb, selectionAnchor, selectionQuad, sharedProps, styleFromProps, textBoxes, unionRect, update, uprightRotation, viewable } from "@embedpdf/core-annotation";
|
|
5
|
+
//#region src/types.ts
|
|
6
|
+
/**
|
|
7
|
+
* The annotation capability token. Typed to the full {@link AnnotationHostCapability}
|
|
8
|
+
* here (the package internals + the `/internal` entry use this view). The package
|
|
9
|
+
* root re-exports the SAME token narrowed to {@link AnnotationCapability}.
|
|
10
|
+
*/
|
|
11
|
+
const AnnotationToken = createCapabilityToken("annotation");
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/repository/seam.ts
|
|
14
|
+
function refKey(ref) {
|
|
15
|
+
return ref.kind === "objectNumber" ? `obj:${ref.annotObjectNumber}` : ref.kind === "nm" ? `nm:${ref.nm}` : `idx:${ref.pageObjectNumber}:${ref.index}`;
|
|
16
|
+
}
|
|
17
|
+
const h2 = (n) => Math.max(0, Math.min(255, Math.round(n))).toString(16).padStart(2, "0");
|
|
18
|
+
const colorToCss = (c) => `#${h2(c.r)}${h2(c.g)}${h2(c.b)}`;
|
|
19
|
+
function cssToColor(css) {
|
|
20
|
+
const s = css.trim();
|
|
21
|
+
const m6 = /^#?([0-9a-f]{6})$/i.exec(s);
|
|
22
|
+
if (m6) {
|
|
23
|
+
const n = parseInt(m6[1], 16);
|
|
24
|
+
return {
|
|
25
|
+
r: n >> 16 & 255,
|
|
26
|
+
g: n >> 8 & 255,
|
|
27
|
+
b: n & 255
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const m3 = /^#?([0-9a-f]{3})$/i.exec(s);
|
|
31
|
+
if (m3) {
|
|
32
|
+
const [a, b, c] = m3[1];
|
|
33
|
+
return {
|
|
34
|
+
r: parseInt(a + a, 16),
|
|
35
|
+
g: parseInt(b + b, 16),
|
|
36
|
+
b: parseInt(c + c, 16)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
r: 0,
|
|
41
|
+
g: 0,
|
|
42
|
+
b: 0
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const toPdfRotation = (rotCW) => normalizeDeg(-rotCW);
|
|
46
|
+
const fromPdfRotation = (rotPdf) => normalizeDeg(-rotPdf);
|
|
47
|
+
/** Advisory `rot` for a vertex geom, from a DTO's (PDF-convention) `rotation`.
|
|
48
|
+
* Absent → no `rot` key (kept off the geom so unrotated shapes stay clean). */
|
|
49
|
+
const rotFromDTO = (rotation) => rotation ? { rot: fromPdfRotation(rotation) } : {};
|
|
50
|
+
/**
|
|
51
|
+
* Geometry/rotation fields for a BOX kind (square/circle/plain free-text). The
|
|
52
|
+
* model's `rect` is the UNROTATED logical box and `rot` the applied tilt, so:
|
|
53
|
+
* - rot == 0 → `/Rect` IS the box; the transform pair is stated as `null`
|
|
54
|
+
* (TOTAL projection — the engine's tri-state writes preserve omissions, so
|
|
55
|
+
* an omitted field would keep a stale rotation instead of flattening it).
|
|
56
|
+
* - rot != 0 → `/Rect` is the rotated visual AABB (PDFium clips the baked /AP
|
|
57
|
+
* to it), `unrotatedRect` the logical box, `rotation` the PDF angle.
|
|
58
|
+
*/
|
|
59
|
+
function boxGeomFields(rect, rot, crop) {
|
|
60
|
+
if (!rot) return {
|
|
61
|
+
rect: contentToPdfRect(rect, crop),
|
|
62
|
+
rotation: null,
|
|
63
|
+
unrotatedRect: null
|
|
64
|
+
};
|
|
65
|
+
return {
|
|
66
|
+
rect: contentToPdfRect(rotatedAabb(rect, rot), crop),
|
|
67
|
+
unrotatedRect: contentToPdfRect(rect, crop),
|
|
68
|
+
rotation: toPdfRotation(rot)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** A box geom (square/circle/stamp) from its DTO: when rotated, the LOCAL box
|
|
72
|
+
* is the stored `unrotatedRect` (the AABB `/Rect` is the rendered envelope)
|
|
73
|
+
* and `rot` the converted tilt; unrotated, `/Rect` IS the box. */
|
|
74
|
+
function boxGeomFromDTO(dto, rotation, unrotatedRect, crop, ellipse) {
|
|
75
|
+
const rot = rotation ? fromPdfRotation(rotation) : 0;
|
|
76
|
+
return {
|
|
77
|
+
t: "rect",
|
|
78
|
+
rect: pdfToContentRect(rot && unrotatedRect ? unrotatedRect : dto.rect, crop),
|
|
79
|
+
ellipse,
|
|
80
|
+
...rot ? { rot } : {}
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
/** Inset a PdfRect by a `/RD` (PDF user space, y-up: all four are non-negative
|
|
84
|
+
* insets from the matching `/Rect` edge). Used to recover the callout text box. */
|
|
85
|
+
const insetPdfRectByRD = (r, rd) => rd ? {
|
|
86
|
+
left: r.left + rd.left,
|
|
87
|
+
bottom: r.bottom + rd.bottom,
|
|
88
|
+
right: r.right - rd.right,
|
|
89
|
+
top: r.top - rd.top
|
|
90
|
+
} : r;
|
|
91
|
+
/** Engine border fields (`/BS /S`, `/BS /D`, `/BE /I`) → the `Border` union. A
|
|
92
|
+
* cloudy effect wins over the underlying border style (which stays solid). */
|
|
93
|
+
function borderFromDTO(d) {
|
|
94
|
+
if ((d.cloudyIntensity ?? 0) > 0) return {
|
|
95
|
+
kind: "cloudy",
|
|
96
|
+
intensity: d.cloudyIntensity
|
|
97
|
+
};
|
|
98
|
+
if (d.borderStyle === "dashed") return {
|
|
99
|
+
kind: "dashed",
|
|
100
|
+
dash: d.dashArray?.length ? d.dashArray : [3, 3]
|
|
101
|
+
};
|
|
102
|
+
return { kind: "solid" };
|
|
103
|
+
}
|
|
104
|
+
/** The writable projection of a `link` value: `goto`/`uri` pass through,
|
|
105
|
+
* read-only arms (`javascript`, `named`, `goto-remote`, `launch`,
|
|
106
|
+
* `unsupported`) yield `null` — they can be carried, never (re)written. */
|
|
107
|
+
function writableTarget(t) {
|
|
108
|
+
return t && (t.kind === "goto" || t.kind === "uri") ? t : null;
|
|
109
|
+
}
|
|
110
|
+
const TEXT_MARKUP = /* @__PURE__ */ new Set([
|
|
111
|
+
"highlight",
|
|
112
|
+
"underline",
|
|
113
|
+
"squiggly",
|
|
114
|
+
"strikeout"
|
|
115
|
+
]);
|
|
116
|
+
const STROKE_KINDS = /* @__PURE__ */ new Set([
|
|
117
|
+
"square",
|
|
118
|
+
"circle",
|
|
119
|
+
"line",
|
|
120
|
+
"polygon",
|
|
121
|
+
"polyline",
|
|
122
|
+
"ink"
|
|
123
|
+
]);
|
|
124
|
+
/** Engine DTO → content-space `Style` (CSS colours, `Border` union). Exported so
|
|
125
|
+
* selection-aware UIs can read display values straight off a {@link AnnotationDTO}
|
|
126
|
+
* without re-deriving the colour/border mapping. */
|
|
127
|
+
function styleFromDTO(dto) {
|
|
128
|
+
if (dto.subtype === "widget") return {
|
|
129
|
+
color: dto.color ? colorToCss(dto.color) : "#1a1a1a",
|
|
130
|
+
interiorColor: dto.interiorColor ? colorToCss(dto.interiorColor) : null,
|
|
131
|
+
strokeWidth: dto.strokeWidth,
|
|
132
|
+
opacity: 1,
|
|
133
|
+
blendMode: dto.blendMode,
|
|
134
|
+
border: dto.borderStyle === "dashed" ? {
|
|
135
|
+
kind: "dashed",
|
|
136
|
+
dash: [3, 3]
|
|
137
|
+
} : { kind: "solid" }
|
|
138
|
+
};
|
|
139
|
+
if (STROKE_KINDS.has(dto.subtype)) {
|
|
140
|
+
const d = dto;
|
|
141
|
+
return {
|
|
142
|
+
color: colorToCss(d.color),
|
|
143
|
+
interiorColor: d.interiorColor ? colorToCss(d.interiorColor) : null,
|
|
144
|
+
strokeWidth: d.strokeWidth,
|
|
145
|
+
opacity: d.opacity,
|
|
146
|
+
blendMode: dto.blendMode,
|
|
147
|
+
border: borderFromDTO(d)
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
if (TEXT_MARKUP.has(dto.subtype)) {
|
|
151
|
+
const d = dto;
|
|
152
|
+
return {
|
|
153
|
+
color: colorToCss(d.color),
|
|
154
|
+
interiorColor: null,
|
|
155
|
+
strokeWidth: 0,
|
|
156
|
+
opacity: d.opacity,
|
|
157
|
+
blendMode: dto.blendMode,
|
|
158
|
+
border: { kind: "solid" }
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
if (dto.subtype === "caret") {
|
|
162
|
+
const d = dto;
|
|
163
|
+
return {
|
|
164
|
+
color: colorToCss(d.color),
|
|
165
|
+
interiorColor: null,
|
|
166
|
+
strokeWidth: 1,
|
|
167
|
+
opacity: d.opacity,
|
|
168
|
+
blendMode: dto.blendMode,
|
|
169
|
+
border: { kind: "solid" }
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
if (dto.subtype === "free-text") {
|
|
173
|
+
const d = dto;
|
|
174
|
+
return {
|
|
175
|
+
color: colorToCss(d.color),
|
|
176
|
+
interiorColor: d.interiorColor ? colorToCss(d.interiorColor) : null,
|
|
177
|
+
strokeWidth: d.strokeWidth,
|
|
178
|
+
opacity: d.opacity,
|
|
179
|
+
blendMode: dto.blendMode,
|
|
180
|
+
border: borderFromDTO(d)
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
if (dto.subtype === "redact") {
|
|
184
|
+
const d = dto;
|
|
185
|
+
return {
|
|
186
|
+
color: colorToCss(d.color),
|
|
187
|
+
interiorColor: d.interiorColor ? colorToCss(d.interiorColor) : null,
|
|
188
|
+
strokeWidth: 1.5,
|
|
189
|
+
opacity: d.opacity,
|
|
190
|
+
blendMode: dto.blendMode,
|
|
191
|
+
border: { kind: "solid" }
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
if (dto.subtype === "text" || dto.subtype === "file-attachment") {
|
|
195
|
+
const d = dto;
|
|
196
|
+
return {
|
|
197
|
+
color: colorToCss(d.color),
|
|
198
|
+
interiorColor: null,
|
|
199
|
+
strokeWidth: 1,
|
|
200
|
+
opacity: d.opacity,
|
|
201
|
+
blendMode: dto.blendMode,
|
|
202
|
+
border: { kind: "solid" }
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
color: "#444444",
|
|
207
|
+
interiorColor: null,
|
|
208
|
+
strokeWidth: 1,
|
|
209
|
+
opacity: 1,
|
|
210
|
+
blendMode: dto.blendMode,
|
|
211
|
+
border: { kind: "solid" }
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* The flat props vocabulary (CSS colours, house keys) → the engine's widget
|
|
216
|
+
* appearance for `doc.forms` authoring — the SAME mapping the widget patch
|
|
217
|
+
* lowering uses, exported as a boundary utility so the form plugin can style
|
|
218
|
+
* `placeField` from a tool's `currentDefaults` without growing a second CSS
|
|
219
|
+
* parser. Absent keys stay absent (the engine writes nothing for them).
|
|
220
|
+
*/
|
|
221
|
+
function widgetAppearanceFromProps(props) {
|
|
222
|
+
return {
|
|
223
|
+
...props.color !== void 0 ? { color: cssToColor(props.color) } : {},
|
|
224
|
+
...props.interiorColor !== void 0 ? { interiorColor: props.interiorColor ? cssToColor(props.interiorColor) : null } : {},
|
|
225
|
+
...props.strokeWidth !== void 0 ? { strokeWidth: props.strokeWidth } : {},
|
|
226
|
+
...props.border !== void 0 ? { borderStyle: props.border.kind === "dashed" ? "dashed" : "solid" } : {},
|
|
227
|
+
...props.fontFamily !== void 0 ? { fontFamily: props.fontFamily } : {},
|
|
228
|
+
...props.fontSize !== void 0 ? { fontSize: props.fontSize } : {},
|
|
229
|
+
...props.fontColor !== void 0 ? { fontColor: cssToColor(props.fontColor) } : {},
|
|
230
|
+
...props.textAlign !== void 0 ? { textAlign: props.textAlign } : {}
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
//#endregion
|
|
234
|
+
//#region src/repository/props.ts
|
|
235
|
+
/**
|
|
236
|
+
* GENERIC per-key prop lowerings — the 1:1 mappings from the flat props
|
|
237
|
+
* vocabulary to wire fields that hold for every kind that declares the key.
|
|
238
|
+
* Kind modules override ONLY their exceptions (a coupling lives in its
|
|
239
|
+
* owner's file: cloudy `/RD` in shape.ts, visual-bounds `/Rect` in stroke.ts,
|
|
240
|
+
* the link target in link.ts).
|
|
241
|
+
*/
|
|
242
|
+
/** /BS slice of the style — a cloudy border keeps a solid underlying stroke
|
|
243
|
+
* (the scallops are the /BE effect, layered on by the shape kinds). */
|
|
244
|
+
const borderSlice = (style) => ({
|
|
245
|
+
borderStyle: style.border.kind === "dashed" ? "dashed" : "solid",
|
|
246
|
+
...style.border.kind === "dashed" ? { dashArray: style.border.dash } : {}
|
|
247
|
+
});
|
|
248
|
+
/** The `/DA`-styled text slice falls back to the draw-time seed exactly like
|
|
249
|
+
* the old projections did (a fresh draft may not carry `text` yet). */
|
|
250
|
+
const textOf = (a) => a.text ?? initialTextStyle;
|
|
251
|
+
const GENERIC_PROPS = {
|
|
252
|
+
color: (a) => ({ color: cssToColor(a.style.color) }),
|
|
253
|
+
opacity: (a) => ({ opacity: a.style.opacity }),
|
|
254
|
+
blendMode: (a) => ({ blendMode: a.style.blendMode }),
|
|
255
|
+
interiorColor: (a) => ({ interiorColor: a.style.interiorColor ? cssToColor(a.style.interiorColor) : null }),
|
|
256
|
+
strokeWidth: (a) => ({ strokeWidth: a.style.strokeWidth }),
|
|
257
|
+
border: (a) => borderSlice(a.style),
|
|
258
|
+
fontFamily: (a) => ({ fontFamily: textOf(a).fontFamily }),
|
|
259
|
+
fontSize: (a) => ({ fontSize: textOf(a).fontSize }),
|
|
260
|
+
fontColor: (a) => ({ fontColor: cssToColor(textOf(a).fontColor) }),
|
|
261
|
+
textAlign: (a) => ({ textAlign: textOf(a).textAlign }),
|
|
262
|
+
icon: (a) => a.icon !== void 0 ? { icon: a.icon } : {},
|
|
263
|
+
link: () => ({})
|
|
264
|
+
};
|
|
265
|
+
//#endregion
|
|
266
|
+
//#region src/repository/projection.ts
|
|
267
|
+
/** Box-kind geometry emission: the model's unrotated `rect` + applied tilt →
|
|
268
|
+
* `/Rect`(+`unrotatedRect`+`rotation`), total (nulls state the clears). */
|
|
269
|
+
const boxEmit = (a, crop) => {
|
|
270
|
+
const g = a.geom;
|
|
271
|
+
return boxGeomFields(g.rect, geomRotation(a.geom), crop);
|
|
272
|
+
};
|
|
273
|
+
//#endregion
|
|
274
|
+
//#region src/repository/kinds/shape.ts
|
|
275
|
+
/** Cloudy `/BE` + `/RD` for a rect shape — TOTAL: the non-cloudy state is
|
|
276
|
+
* stated as `null` (tri-state remove), never omitted, so toggling cloudy OFF
|
|
277
|
+
* removes the stale inset (the Adobe phantom-padding bug). */
|
|
278
|
+
function cloudyExtras(a) {
|
|
279
|
+
if (a.geom.t !== "rect") return {};
|
|
280
|
+
if (a.style.border.kind === "cloudy") {
|
|
281
|
+
const inset = cloudyBorderExtent(a.style.border.intensity, a.style.strokeWidth, a.geom.ellipse);
|
|
282
|
+
return {
|
|
283
|
+
cloudyIntensity: a.style.border.intensity,
|
|
284
|
+
rectDifferences: {
|
|
285
|
+
left: inset,
|
|
286
|
+
top: inset,
|
|
287
|
+
right: inset,
|
|
288
|
+
bottom: inset
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
cloudyIntensity: null,
|
|
294
|
+
rectDifferences: null
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const ingest = (dto, crop, ellipse) => {
|
|
298
|
+
const d = dto;
|
|
299
|
+
return { geom: boxGeomFromDTO(d, d.rotation, d.unrotatedRect, crop, ellipse) };
|
|
300
|
+
};
|
|
301
|
+
const projection = (ellipse) => ({
|
|
302
|
+
ingest: (dto, crop) => ingest(dto, crop, ellipse),
|
|
303
|
+
geometry: (a, crop) => boxEmit(a, crop),
|
|
304
|
+
prop: {
|
|
305
|
+
strokeWidth: (a) => ({
|
|
306
|
+
strokeWidth: a.style.strokeWidth,
|
|
307
|
+
...cloudyExtras(a)
|
|
308
|
+
}),
|
|
309
|
+
border: (a) => ({
|
|
310
|
+
...borderSlice(a.style),
|
|
311
|
+
...cloudyExtras(a)
|
|
312
|
+
})
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
const square = projection(false);
|
|
316
|
+
const circle = projection(true);
|
|
317
|
+
//#endregion
|
|
318
|
+
//#region src/repository/kinds/stroke.ts
|
|
319
|
+
/** Advisory rotation, TOTAL: rotation 0 states `null` (tri-state clear) —
|
|
320
|
+
* omission would preserve a stale advisory angle. */
|
|
321
|
+
const advisoryRotation = (g) => {
|
|
322
|
+
const rot = geomRotation(g);
|
|
323
|
+
return { rotation: rot ? toPdfRotation(rot) : null };
|
|
324
|
+
};
|
|
325
|
+
/** `/BE` intensity for a closed poly (polygon): the curls are generated from
|
|
326
|
+
* /Vertices + /BE alone — per ISO 32000 no /RD applies — and the /Rect
|
|
327
|
+
* (geomPdfBounds WITH the border) already includes the outward cloud extent. */
|
|
328
|
+
const polyCloudy = (a) => a.geom.t === "poly" && a.geom.closed ? { cloudyIntensity: a.style.border.kind === "cloudy" ? a.style.border.intensity : null } : {};
|
|
329
|
+
/** The visual-bounds `/Rect` (stroke + border included) of a stroke geom. */
|
|
330
|
+
const visualRect = (a, crop) => {
|
|
331
|
+
const g = a.geom;
|
|
332
|
+
if (g.t === "line" || g.t === "ink") return { rect: geomPdfBounds(g, a.style.strokeWidth, crop) };
|
|
333
|
+
if (g.t === "poly") return { rect: geomPdfBounds(g, a.style.strokeWidth, crop, a.style.border) };
|
|
334
|
+
return {};
|
|
335
|
+
};
|
|
336
|
+
/** Stroke-family prop exceptions: width and border feed the derived /Rect. */
|
|
337
|
+
const strokeProps = {
|
|
338
|
+
strokeWidth: (a, crop) => ({
|
|
339
|
+
strokeWidth: a.style.strokeWidth,
|
|
340
|
+
...visualRect(a, crop)
|
|
341
|
+
}),
|
|
342
|
+
border: (a, crop) => ({
|
|
343
|
+
...borderSlice(a.style),
|
|
344
|
+
...polyCloudy(a),
|
|
345
|
+
...a.geom.t === "poly" && a.geom.closed ? visualRect(a, crop) : {}
|
|
346
|
+
}),
|
|
347
|
+
lineEndings: (a) => (a.geom.t === "line" || a.geom.t === "poly") && a.geom.ends ? { lineEndings: a.geom.ends } : {}
|
|
348
|
+
};
|
|
349
|
+
const line = {
|
|
350
|
+
ingest: (dto, crop) => {
|
|
351
|
+
const d = dto;
|
|
352
|
+
return { geom: {
|
|
353
|
+
t: "line",
|
|
354
|
+
a: pdfToContentPoint(d.linePoints.start, crop),
|
|
355
|
+
b: pdfToContentPoint(d.linePoints.end, crop),
|
|
356
|
+
ends: d.lineEndings,
|
|
357
|
+
...rotFromDTO(d.rotation)
|
|
358
|
+
} };
|
|
359
|
+
},
|
|
360
|
+
geometry: (a, crop) => {
|
|
361
|
+
const g = a.geom;
|
|
362
|
+
if (g.t !== "line") return null;
|
|
363
|
+
return {
|
|
364
|
+
linePoints: {
|
|
365
|
+
start: contentToPdfPoint(g.a, crop),
|
|
366
|
+
end: contentToPdfPoint(g.b, crop)
|
|
367
|
+
},
|
|
368
|
+
...visualRect(a, crop),
|
|
369
|
+
...advisoryRotation(g)
|
|
370
|
+
};
|
|
371
|
+
},
|
|
372
|
+
prop: strokeProps
|
|
373
|
+
};
|
|
374
|
+
const polyProjection = (closed) => ({
|
|
375
|
+
ingest: (dto, crop) => {
|
|
376
|
+
const d = dto;
|
|
377
|
+
return { geom: {
|
|
378
|
+
t: "poly",
|
|
379
|
+
points: d.vertices.map((p) => pdfToContentPoint(p, crop)),
|
|
380
|
+
closed,
|
|
381
|
+
..."lineEndings" in d ? { ends: d.lineEndings } : {},
|
|
382
|
+
...rotFromDTO(d.rotation)
|
|
383
|
+
} };
|
|
384
|
+
},
|
|
385
|
+
geometry: (a, crop) => {
|
|
386
|
+
const g = a.geom;
|
|
387
|
+
if (g.t !== "poly") return null;
|
|
388
|
+
return {
|
|
389
|
+
vertices: g.points.map((p) => contentToPdfPoint(p, crop)),
|
|
390
|
+
...visualRect(a, crop),
|
|
391
|
+
...advisoryRotation(g)
|
|
392
|
+
};
|
|
393
|
+
},
|
|
394
|
+
prop: strokeProps
|
|
395
|
+
});
|
|
396
|
+
const polygon = polyProjection(true);
|
|
397
|
+
const polyline = polyProjection(false);
|
|
398
|
+
const ink = {
|
|
399
|
+
ingest: (dto, crop) => {
|
|
400
|
+
const d = dto;
|
|
401
|
+
return {
|
|
402
|
+
geom: {
|
|
403
|
+
t: "ink",
|
|
404
|
+
strokes: d.inkList.map((stroke) => stroke.map((p) => pdfToContentPoint(p, crop))),
|
|
405
|
+
...rotFromDTO(d.rotation)
|
|
406
|
+
},
|
|
407
|
+
...d.intent ? { intent: d.intent } : {}
|
|
408
|
+
};
|
|
409
|
+
},
|
|
410
|
+
geometry: (a, crop) => {
|
|
411
|
+
const g = a.geom;
|
|
412
|
+
if (g.t !== "ink") return null;
|
|
413
|
+
return {
|
|
414
|
+
inkList: g.strokes.map((stroke) => stroke.map((p) => contentToPdfPoint(p, crop))),
|
|
415
|
+
...visualRect(a, crop),
|
|
416
|
+
...advisoryRotation(g)
|
|
417
|
+
};
|
|
418
|
+
},
|
|
419
|
+
prop: strokeProps,
|
|
420
|
+
draftExtras: (a) => a.intent === "ink-highlight" ? { intent: a.intent } : {}
|
|
421
|
+
};
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/repository/kinds/freeText.ts
|
|
424
|
+
/** Free-text `/DA` fields → content {@link TextStyle}. An absent `fontColor`
|
|
425
|
+
* falls back to the `/DA` colour — the same rule the CPVT renderer applies. */
|
|
426
|
+
function textFromDTO(dto) {
|
|
427
|
+
return {
|
|
428
|
+
fontFamily: dto.fontFamily,
|
|
429
|
+
fontSize: dto.fontSize,
|
|
430
|
+
fontColor: colorToCss(dto.fontColor ?? dto.color),
|
|
431
|
+
textAlign: dto.textAlign
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* The engine geometry for a callout: the overall `/Rect` (text box ∪ leader ∪
|
|
436
|
+
* arrow), the `/RD` inset that recovers the text box from it, the `/CL` leader
|
|
437
|
+
* (`[tip, knee, conn]` with the connection point derived), and the `/LE`
|
|
438
|
+
* ending. All in PDF user space (y-up), with every `/RD` inset clamped
|
|
439
|
+
* non-negative.
|
|
440
|
+
*
|
|
441
|
+
* A tilted box (the upright policy) additionally emits `rotation` +
|
|
442
|
+
* `unrotatedRect` (the logical text box) — the engine bakes the box + text
|
|
443
|
+
* under an INLINE rotation about the box centre while the leader stays
|
|
444
|
+
* page-space, so (unlike plain boxes) the /AP form `/Matrix` stays identity
|
|
445
|
+
* and the raster stays placed by `/Rect`. `/RD` then insets to the ROTATED
|
|
446
|
+
* box's AABB — the best axis-aligned text box a foreign viewer regenerating
|
|
447
|
+
* the AP can draw (spec-conformant degradation). The transform pair is TOTAL
|
|
448
|
+
* (nulls state the clears) like every box emission.
|
|
449
|
+
*/
|
|
450
|
+
function calloutFields(a, crop) {
|
|
451
|
+
const g = a.geom;
|
|
452
|
+
if (g.t !== "text" || !g.callout) return null;
|
|
453
|
+
const rot = geomRotation(g);
|
|
454
|
+
const overall = geomPdfBounds(g, a.style.strokeWidth, crop);
|
|
455
|
+
const tb = contentToPdfRect$1(rot ? rotatedAabb(g.rect, rot) : g.rect, crop);
|
|
456
|
+
const nn = (n) => Math.max(0, n);
|
|
457
|
+
const pts = calloutLinePoints(g).map((p) => contentToPdfPoint(p, crop));
|
|
458
|
+
const calloutLine = pts.length === 3 ? [
|
|
459
|
+
pts[0],
|
|
460
|
+
pts[1],
|
|
461
|
+
pts[2]
|
|
462
|
+
] : [pts[0], pts[1]];
|
|
463
|
+
return {
|
|
464
|
+
rect: overall,
|
|
465
|
+
rectDifferences: {
|
|
466
|
+
left: nn(tb.left - overall.left),
|
|
467
|
+
bottom: nn(tb.bottom - overall.bottom),
|
|
468
|
+
right: nn(overall.right - tb.right),
|
|
469
|
+
top: nn(overall.top - tb.top)
|
|
470
|
+
},
|
|
471
|
+
calloutLine,
|
|
472
|
+
lineEnding: g.callout.ending,
|
|
473
|
+
...rot ? {
|
|
474
|
+
rotation: toPdfRotation(rot),
|
|
475
|
+
unrotatedRect: contentToPdfRect$1(g.rect, crop)
|
|
476
|
+
} : {
|
|
477
|
+
rotation: null,
|
|
478
|
+
unrotatedRect: null
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
const freeText = {
|
|
483
|
+
ingest: (dto, crop) => {
|
|
484
|
+
const d = dto;
|
|
485
|
+
if (d.intent === "free-text-callout" && d.calloutLine && d.calloutLine.length >= 2) {
|
|
486
|
+
const cl = d.calloutLine;
|
|
487
|
+
const rot = d.rotation ? fromPdfRotation(d.rotation) : 0;
|
|
488
|
+
return {
|
|
489
|
+
geom: {
|
|
490
|
+
t: "text",
|
|
491
|
+
rect: rot && d.unrotatedRect ? pdfToContentRect$1(d.unrotatedRect, crop) : pdfToContentRect$1(insetPdfRectByRD(d.rect, d.rectDifferences), crop),
|
|
492
|
+
callout: {
|
|
493
|
+
tip: pdfToContentPoint$1(cl[0], crop),
|
|
494
|
+
knee: cl.length === 3 ? pdfToContentPoint$1(cl[1], crop) : void 0,
|
|
495
|
+
ending: d.lineEnding ?? "none"
|
|
496
|
+
},
|
|
497
|
+
...rot && d.unrotatedRect ? { rot } : {}
|
|
498
|
+
},
|
|
499
|
+
text: textFromDTO(d)
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
const rot = d.rotation ? fromPdfRotation(d.rotation) : 0;
|
|
503
|
+
return {
|
|
504
|
+
geom: {
|
|
505
|
+
t: "text",
|
|
506
|
+
rect: pdfToContentRect$1(rot && d.unrotatedRect ? d.unrotatedRect : d.rect, crop),
|
|
507
|
+
...rot ? { rot } : {}
|
|
508
|
+
},
|
|
509
|
+
text: textFromDTO(d)
|
|
510
|
+
};
|
|
511
|
+
},
|
|
512
|
+
geometry: (a, crop) => {
|
|
513
|
+
if (a.geom.t !== "text") return null;
|
|
514
|
+
const cf = calloutFields(a, crop);
|
|
515
|
+
if (cf) return { ...cf };
|
|
516
|
+
return boxEmit(a, crop);
|
|
517
|
+
},
|
|
518
|
+
draftExtras: (a, crop) => ({
|
|
519
|
+
intent: calloutFields(a, crop) ? "free-text-callout" : "free-text",
|
|
520
|
+
contents: a.data?.contents ?? ""
|
|
521
|
+
})
|
|
522
|
+
};
|
|
523
|
+
//#endregion
|
|
524
|
+
//#region src/repository/kinds/quads.ts
|
|
525
|
+
const quadsFromDTO = (quadPoints, crop) => quadPoints.map((q) => [
|
|
526
|
+
pdfToContentPoint$1(q.p1, crop),
|
|
527
|
+
pdfToContentPoint$1(q.p2, crop),
|
|
528
|
+
pdfToContentPoint$1(q.p3, crop),
|
|
529
|
+
pdfToContentPoint$1(q.p4, crop)
|
|
530
|
+
]);
|
|
531
|
+
/** Content quads → engine `/QuadPoints` (PDF user space); null off quads geom. */
|
|
532
|
+
function quadPointsFor(a, crop) {
|
|
533
|
+
if (a.geom.t !== "quads") return null;
|
|
534
|
+
return a.geom.quads.map((q) => ({
|
|
535
|
+
p1: contentToPdfPoint(q[0], crop),
|
|
536
|
+
p2: contentToPdfPoint(q[1], crop),
|
|
537
|
+
p3: contentToPdfPoint(q[2], crop),
|
|
538
|
+
p4: contentToPdfPoint(q[3], crop)
|
|
539
|
+
}));
|
|
540
|
+
}
|
|
541
|
+
/** PDF-space bounding box of a set of `/QuadPoints` quads — the `/Rect` a
|
|
542
|
+
* quad-bearing draft must carry alongside its quads. */
|
|
543
|
+
const pdfBoundsOfQuads = (quads) => {
|
|
544
|
+
let left = Infinity;
|
|
545
|
+
let bottom = Infinity;
|
|
546
|
+
let right = -Infinity;
|
|
547
|
+
let top = -Infinity;
|
|
548
|
+
for (const q of quads) for (const p of [
|
|
549
|
+
q.p1,
|
|
550
|
+
q.p2,
|
|
551
|
+
q.p3,
|
|
552
|
+
q.p4
|
|
553
|
+
]) {
|
|
554
|
+
if (p.x < left) left = p.x;
|
|
555
|
+
if (p.x > right) right = p.x;
|
|
556
|
+
if (p.y < bottom) bottom = p.y;
|
|
557
|
+
if (p.y > top) top = p.y;
|
|
558
|
+
}
|
|
559
|
+
return {
|
|
560
|
+
left,
|
|
561
|
+
bottom,
|
|
562
|
+
right,
|
|
563
|
+
top
|
|
564
|
+
};
|
|
565
|
+
};
|
|
566
|
+
const markupProjection = (subtype) => {
|
|
567
|
+
return {
|
|
568
|
+
ingest: (dto, crop) => {
|
|
569
|
+
const d = dto;
|
|
570
|
+
return {
|
|
571
|
+
geom: {
|
|
572
|
+
t: "quads",
|
|
573
|
+
quads: quadsFromDTO(d.quadPoints, crop)
|
|
574
|
+
},
|
|
575
|
+
...subtype === "strikeout" && "intent" in d && d.intent ? { intent: d.intent } : {}
|
|
576
|
+
};
|
|
577
|
+
},
|
|
578
|
+
geometry: () => null,
|
|
579
|
+
draftExtras: (a, crop) => {
|
|
580
|
+
const quads = quadPointsFor(a, crop);
|
|
581
|
+
if (!quads) return null;
|
|
582
|
+
return {
|
|
583
|
+
quadPoints: quads,
|
|
584
|
+
...subtype === "strikeout" && a.intent === "strikeout-text-edit" ? { intent: a.intent } : {}
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
};
|
|
589
|
+
const highlight = markupProjection("highlight");
|
|
590
|
+
const underline = markupProjection("underline");
|
|
591
|
+
const squiggly = markupProjection("squiggly");
|
|
592
|
+
const strikeout = markupProjection("strikeout");
|
|
593
|
+
const caret = {
|
|
594
|
+
ingest: (dto, crop) => {
|
|
595
|
+
const d = dto;
|
|
596
|
+
return {
|
|
597
|
+
geom: {
|
|
598
|
+
t: "caret",
|
|
599
|
+
rect: pdfToContentRect$1(d.rect, crop)
|
|
600
|
+
},
|
|
601
|
+
...d.intent ? { intent: d.intent } : {}
|
|
602
|
+
};
|
|
603
|
+
},
|
|
604
|
+
geometry: (a, crop) => a.geom.t === "caret" ? { rect: contentToPdfRect$1(a.geom.rect, crop) } : null,
|
|
605
|
+
draftExtras: (a) => ({
|
|
606
|
+
rectDifferences: {
|
|
607
|
+
left: .5,
|
|
608
|
+
top: .5,
|
|
609
|
+
right: .5,
|
|
610
|
+
bottom: .5
|
|
611
|
+
},
|
|
612
|
+
...a.intent === "replace" ? { intent: a.intent } : {},
|
|
613
|
+
...a.data?.contents != null ? { contents: a.data.contents } : {}
|
|
614
|
+
})
|
|
615
|
+
};
|
|
616
|
+
const redact = {
|
|
617
|
+
ingest: (dto, crop) => {
|
|
618
|
+
const d = dto;
|
|
619
|
+
return {
|
|
620
|
+
geom: d.quadPoints.length > 0 ? {
|
|
621
|
+
t: "quads",
|
|
622
|
+
quads: quadsFromDTO(d.quadPoints, crop)
|
|
623
|
+
} : {
|
|
624
|
+
t: "rect",
|
|
625
|
+
rect: pdfToContentRect$1(d.rect, crop),
|
|
626
|
+
ellipse: false
|
|
627
|
+
},
|
|
628
|
+
text: {
|
|
629
|
+
fontFamily: d.fontFamily,
|
|
630
|
+
fontSize: d.fontSize,
|
|
631
|
+
fontColor: colorToCss(d.fontColor),
|
|
632
|
+
textAlign: d.textAlign
|
|
633
|
+
},
|
|
634
|
+
...d.overlayText ? { label: {
|
|
635
|
+
text: d.overlayText,
|
|
636
|
+
repeat: d.repeat
|
|
637
|
+
} } : {}
|
|
638
|
+
};
|
|
639
|
+
},
|
|
640
|
+
geometry: (a, crop) => a.geom.t === "rect" ? { rect: contentToPdfRect$1(a.geom.rect, crop) } : null,
|
|
641
|
+
draftExtras: (a, crop) => {
|
|
642
|
+
const quads = quadPointsFor(a, crop);
|
|
643
|
+
return quads ? {
|
|
644
|
+
quadPoints: quads,
|
|
645
|
+
rect: pdfBoundsOfQuads(quads)
|
|
646
|
+
} : {};
|
|
647
|
+
}
|
|
648
|
+
};
|
|
649
|
+
//#endregion
|
|
650
|
+
//#region src/repository/kinds/misc.ts
|
|
651
|
+
const rectGeometry = (a, crop) => "rect" in a.geom ? { rect: contentToPdfRect$1(a.geom.rect, crop) } : null;
|
|
652
|
+
const iconProjection = (subtype) => ({
|
|
653
|
+
ingest: (dto, crop) => {
|
|
654
|
+
const d = dto;
|
|
655
|
+
return {
|
|
656
|
+
geom: {
|
|
657
|
+
t: "rect",
|
|
658
|
+
rect: pdfToContentRect$1(d.rect, crop),
|
|
659
|
+
ellipse: false
|
|
660
|
+
},
|
|
661
|
+
icon: d.icon
|
|
662
|
+
};
|
|
663
|
+
},
|
|
664
|
+
geometry: rectGeometry,
|
|
665
|
+
createable: false
|
|
666
|
+
});
|
|
667
|
+
const textNote = iconProjection("text");
|
|
668
|
+
const fileAttachment = iconProjection("file-attachment");
|
|
669
|
+
const stamp = {
|
|
670
|
+
ingest: (dto, crop) => {
|
|
671
|
+
const d = dto;
|
|
672
|
+
return { geom: boxGeomFromDTO(d, d.rotation, d.unrotatedRect, crop, false) };
|
|
673
|
+
},
|
|
674
|
+
geometry: (a, crop) => boxEmit(a, crop),
|
|
675
|
+
createable: false
|
|
676
|
+
};
|
|
677
|
+
const link = {
|
|
678
|
+
ingest: (dto, crop) => {
|
|
679
|
+
const d = dto;
|
|
680
|
+
return {
|
|
681
|
+
geom: {
|
|
682
|
+
t: "rect",
|
|
683
|
+
rect: pdfToContentRect$1(d.rect, crop),
|
|
684
|
+
ellipse: false
|
|
685
|
+
},
|
|
686
|
+
link: d.target
|
|
687
|
+
};
|
|
688
|
+
},
|
|
689
|
+
geometry: rectGeometry,
|
|
690
|
+
prop: { link: (a) => {
|
|
691
|
+
const target = writableTarget(a.link);
|
|
692
|
+
return target ? { target } : a.link == null ? { target: null } : {};
|
|
693
|
+
} },
|
|
694
|
+
draftExtras: (a) => ({ target: writableTarget(a.link) })
|
|
695
|
+
};
|
|
696
|
+
/** One PDF `widget` subtype → per-family CLIENT kinds (radios have no font). */
|
|
697
|
+
const WIDGET_KIND_BY_FAMILY = {
|
|
698
|
+
text: "widget-text",
|
|
699
|
+
combobox: "widget-choice",
|
|
700
|
+
listbox: "widget-choice",
|
|
701
|
+
pushbutton: "widget-button",
|
|
702
|
+
checkbox: "widget-toggle",
|
|
703
|
+
radio: "widget-toggle"
|
|
704
|
+
};
|
|
705
|
+
const widgetKindOf = (family) => WIDGET_KIND_BY_FAMILY[family] ?? "widget-box";
|
|
706
|
+
const WIDGET_TEXT_KINDS = /* @__PURE__ */ new Set([
|
|
707
|
+
"widget-text",
|
|
708
|
+
"widget-choice",
|
|
709
|
+
"widget-button"
|
|
710
|
+
]);
|
|
711
|
+
function widgetTextFromDTO(dto) {
|
|
712
|
+
return {
|
|
713
|
+
fontFamily: dto.fontFamily ?? "helvetica",
|
|
714
|
+
fontSize: dto.fontSize ?? 0,
|
|
715
|
+
fontColor: dto.fontColor ? colorToCss(dto.fontColor) : "#000000",
|
|
716
|
+
textAlign: dto.textAlign
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
const widget = {
|
|
720
|
+
ingest: (dto, crop) => {
|
|
721
|
+
const d = dto;
|
|
722
|
+
return {
|
|
723
|
+
geom: boxGeomFromDTO(d, void 0, void 0, crop, false),
|
|
724
|
+
...WIDGET_TEXT_KINDS.has(widgetKindOf(d.fieldFamily)) ? { text: widgetTextFromDTO(d) } : {}
|
|
725
|
+
};
|
|
726
|
+
},
|
|
727
|
+
geometry: rectGeometry,
|
|
728
|
+
createable: false
|
|
729
|
+
};
|
|
730
|
+
const unsupported = {
|
|
731
|
+
ingest: (dto, crop) => ({ geom: {
|
|
732
|
+
t: "rect",
|
|
733
|
+
rect: pdfToContentRect$1(dto.rect, crop),
|
|
734
|
+
ellipse: false
|
|
735
|
+
} }),
|
|
736
|
+
geometry: () => null,
|
|
737
|
+
createable: false
|
|
738
|
+
};
|
|
739
|
+
//#endregion
|
|
740
|
+
//#region src/repository/links.ts
|
|
741
|
+
/**
|
|
742
|
+
* Attached-link folding + desired-state derivation — RELATIONSHIP logic, not
|
|
743
|
+
* projection: a `/Link` child grouped under a linkable parent becomes the
|
|
744
|
+
* parent's `link` prop, and the reconciler derives the children it should
|
|
745
|
+
* have from the parent's committed geometry.
|
|
746
|
+
*/
|
|
747
|
+
/** Does this kind's table declare the `link` prop (may it carry an attached
|
|
748
|
+
* link)? Widgets/caret/redact/file-attachment deliberately don't. */
|
|
749
|
+
const takesLink = (subtype) => propsFor(subtype).some((s) => s.key === "link");
|
|
750
|
+
/**
|
|
751
|
+
* Fold attached link children onto their parents: a `/Link` annotation
|
|
752
|
+
* grouped (`/RT /Group`) under a linkable non-link parent is not an object
|
|
753
|
+
* in the client model — it becomes the parent's `link` prop value plus the
|
|
754
|
+
* `linkRefs` join keys the write side needs. Folded children are REMOVED
|
|
755
|
+
* from the returned list, so they never render, hit-test, or select.
|
|
756
|
+
* Anything that doesn't match (orphans, links grouped under widgets or
|
|
757
|
+
* other links) stays a standalone annotation — nothing is ever hidden.
|
|
758
|
+
*
|
|
759
|
+
* Multi-segment parents (markup) carry several children with one shared
|
|
760
|
+
* target; the first child's target wins and the next write heals any
|
|
761
|
+
* foreign disagreement.
|
|
762
|
+
*/
|
|
763
|
+
function foldAttachedLinks(annots) {
|
|
764
|
+
const byId = new Map(annots.map((a) => [a.id, a]));
|
|
765
|
+
const folded = /* @__PURE__ */ new Set();
|
|
766
|
+
const patched = /* @__PURE__ */ new Map();
|
|
767
|
+
for (const a of annots) {
|
|
768
|
+
if (a.subtype !== "link" || a.data?.subtype !== "link") continue;
|
|
769
|
+
if (a.data.replyType !== "group" || !a.data.inReplyTo || !a.ref) continue;
|
|
770
|
+
const parentId = refKey(a.data.inReplyTo);
|
|
771
|
+
const parent = patched.get(parentId) ?? byId.get(parentId);
|
|
772
|
+
if (!parent || parent.subtype === "link" || !takesLink(parent.subtype)) continue;
|
|
773
|
+
patched.set(parentId, {
|
|
774
|
+
...parent,
|
|
775
|
+
link: parent.link ?? a.data.target,
|
|
776
|
+
linkRefs: [...parent.linkRefs ?? [], a.ref]
|
|
777
|
+
});
|
|
778
|
+
folded.add(a.id);
|
|
779
|
+
}
|
|
780
|
+
if (!folded.size) return annots;
|
|
781
|
+
return annots.filter((a) => !folded.has(a.id)).map((a) => patched.get(a.id) ?? a);
|
|
782
|
+
}
|
|
783
|
+
/** Bounds of one quad (content space). */
|
|
784
|
+
const quadBounds = (q) => unionRect(q);
|
|
785
|
+
/**
|
|
786
|
+
* The DESIRED hit rects (content space) of a parent's attached link
|
|
787
|
+
* children — derived fresh from the parent's committed geometry, never
|
|
788
|
+
* tracked: markup gets one child per quad (per-line hit areas, the v2
|
|
789
|
+
* segment behaviour), every other kind one child over its visual bounds.
|
|
790
|
+
* The reconciler and the navigation lens both read THIS, so the clickable
|
|
791
|
+
* area and the written `/Rect`s can never disagree.
|
|
792
|
+
*
|
|
793
|
+
* A link's `/Rect` is axis-aligned by spec (no rotation exists for links),
|
|
794
|
+
* so a ROTATED parent gets the AABB of its rotated footprint — the
|
|
795
|
+
* `selectionQuad` corners (rotation + stroke included), the same envelope
|
|
796
|
+
* the selection chrome outlines. Exact rotated hit regions need
|
|
797
|
+
* `/QuadPoints` (tier 2).
|
|
798
|
+
*/
|
|
799
|
+
function linkChildRects(a) {
|
|
800
|
+
if (a.geom.t === "quads") return a.geom.quads.map(quadBounds);
|
|
801
|
+
return [unionRect(selectionQuad(a.geom, a.style.strokeWidth, a.style.border))];
|
|
802
|
+
}
|
|
803
|
+
//#endregion
|
|
804
|
+
//#region src/repository/index.ts
|
|
805
|
+
/** Every wire subtype declares exactly one projection — a missing kind is a
|
|
806
|
+
* COMPILE error, not a silent fall-through. */
|
|
807
|
+
const KINDS = {
|
|
808
|
+
square,
|
|
809
|
+
circle,
|
|
810
|
+
line,
|
|
811
|
+
polygon,
|
|
812
|
+
polyline,
|
|
813
|
+
ink,
|
|
814
|
+
"free-text": freeText,
|
|
815
|
+
highlight,
|
|
816
|
+
underline,
|
|
817
|
+
squiggly,
|
|
818
|
+
strikeout,
|
|
819
|
+
caret,
|
|
820
|
+
redact,
|
|
821
|
+
text: textNote,
|
|
822
|
+
"file-attachment": fileAttachment,
|
|
823
|
+
stamp,
|
|
824
|
+
link,
|
|
825
|
+
widget,
|
|
826
|
+
unsupported
|
|
827
|
+
};
|
|
828
|
+
/** Model subtypes are wire subtypes except the widget CLIENT kinds
|
|
829
|
+
* (`widget-text`…), which all project through the widget family. */
|
|
830
|
+
const projectionOf = (subtype) => subtype.startsWith("widget") ? KINDS.widget : KINDS[subtype] ?? KINDS.unsupported;
|
|
831
|
+
const wireSubtypeOf = (a) => a.subtype.startsWith("widget") ? "widget" : a.subtype;
|
|
832
|
+
/**
|
|
833
|
+
* Engine DTO → content-space Annot. `source` decides how it renders: `'baked'`
|
|
834
|
+
* shows the engine's appearance raster (a page load, or a remote edit — trust
|
|
835
|
+
* the authored AP); `'vector'` renders live from geom/style (we authored or
|
|
836
|
+
* changed it). `apBox` is the raster's content-space box, used while baked.
|
|
837
|
+
*/
|
|
838
|
+
function fromDTO(dto, crop, source = "baked") {
|
|
839
|
+
const slice = projectionOf(dto.subtype).ingest(dto, crop);
|
|
840
|
+
const strippedRect = !(dto.subtype === "free-text" && dto.intent === "free-text-callout") && "unrotatedRect" in dto && "rotation" in dto && dto.rotation ? dto.unrotatedRect : void 0;
|
|
841
|
+
return {
|
|
842
|
+
id: refKey(dto.ref),
|
|
843
|
+
ref: dto.ref,
|
|
844
|
+
pon: dto.pageObjectNumber,
|
|
845
|
+
subtype: dto.subtype === "widget" ? widgetKindOf(dto.fieldFamily) : dto.subtype,
|
|
846
|
+
flags: dto.flags,
|
|
847
|
+
source,
|
|
848
|
+
data: dto,
|
|
849
|
+
...dto.inReplyTo ? { irt: refKey(dto.inReplyTo) } : {},
|
|
850
|
+
...dto.replyType === "group" && dto.inReplyTo ? { group: refKey(dto.inReplyTo) } : {},
|
|
851
|
+
style: styleFromDTO(dto),
|
|
852
|
+
...slice,
|
|
853
|
+
apBox: pdfToContentRect$1(strippedRect ?? dto.rect, crop),
|
|
854
|
+
...strippedRect ? { apRot: geomRotation(slice.geom) } : {}
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
/** Lower `keys` through the kind's overrides + the generic table. `null` =
|
|
858
|
+
* some key has no lowering — the caller degrades to the full projection. */
|
|
859
|
+
function emitProps(a, crop, keys) {
|
|
860
|
+
const kind = projectionOf(a.subtype);
|
|
861
|
+
const out = {};
|
|
862
|
+
for (const key of keys) {
|
|
863
|
+
const lower = kind.prop?.[key] ?? GENERIC_PROPS[key];
|
|
864
|
+
if (!lower) return null;
|
|
865
|
+
Object.assign(out, lower(a, crop));
|
|
866
|
+
}
|
|
867
|
+
return out;
|
|
868
|
+
}
|
|
869
|
+
const editableKeys = (subtype) => propsFor(subtype).map((s) => s.key);
|
|
870
|
+
/** Content Annot → the FULL engine patch: the kind's geometry group plus every
|
|
871
|
+
* prop it declares editable. The reference statement — scoped emission and
|
|
872
|
+
* drafts both build on it. */
|
|
873
|
+
function toPatch(a, crop) {
|
|
874
|
+
const geo = projectionOf(a.subtype).geometry(a, crop);
|
|
875
|
+
const props = emitProps(a, crop, editableKeys(a.subtype)) ?? {};
|
|
876
|
+
if (!geo && Object.keys(props).length === 0) return null;
|
|
877
|
+
return {
|
|
878
|
+
subtype: wireSubtypeOf(a),
|
|
879
|
+
...geo,
|
|
880
|
+
...props
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
/**
|
|
884
|
+
* Content Annot + a {@link PatchScope} → the SPARSE patch for exactly that
|
|
885
|
+
* intent (the shell's `patch` effect emitter): the reducer says WHAT changed —
|
|
886
|
+
* geometry, or the props keys verbatim — and this lowers only that. Kinds
|
|
887
|
+
* without editable geometry (text markup) and unlowerable keys degrade to the
|
|
888
|
+
* full patch: verbose, never a dropped write.
|
|
889
|
+
*/
|
|
890
|
+
function toScopedPatch(a, scope, crop) {
|
|
891
|
+
const kind = projectionOf(a.subtype);
|
|
892
|
+
if (scope.kind === "geometry") {
|
|
893
|
+
const geo = kind.geometry(a, crop);
|
|
894
|
+
return geo ? {
|
|
895
|
+
subtype: wireSubtypeOf(a),
|
|
896
|
+
...geo
|
|
897
|
+
} : toPatch(a, crop);
|
|
898
|
+
}
|
|
899
|
+
const props = emitProps(a, crop, scope.keys);
|
|
900
|
+
if (props === null) return toPatch(a, crop);
|
|
901
|
+
if (Object.keys(props).length === 0) return null;
|
|
902
|
+
return {
|
|
903
|
+
subtype: wireSubtypeOf(a),
|
|
904
|
+
...props
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
/** Content Annot → engine create draft: the full statement plus the kind's
|
|
908
|
+
* create-only extras, with the model's `/F` emitted verbatim, once, for every
|
|
909
|
+
* kind (a fresh draw carries DRAWN_FLAGS plus any tool seed). `null` for the
|
|
910
|
+
* kinds whose creates travel their own path (stamps, widgets, icon place). */
|
|
911
|
+
function toCreateDraft(a, crop) {
|
|
912
|
+
const kind = projectionOf(a.subtype);
|
|
913
|
+
if (kind.createable === false) return null;
|
|
914
|
+
const base = toPatch(a, crop);
|
|
915
|
+
if (!base) return null;
|
|
916
|
+
const extras = kind.draftExtras?.(a, crop);
|
|
917
|
+
if (kind.draftExtras && extras === null) return null;
|
|
918
|
+
return {
|
|
919
|
+
...base,
|
|
920
|
+
...extras,
|
|
921
|
+
flags: a.flags
|
|
922
|
+
};
|
|
923
|
+
}
|
|
924
|
+
//#endregion
|
|
925
|
+
//#region src/text-item.ts
|
|
926
|
+
/**
|
|
927
|
+
* The free-text presentation projection: the core's geometry-only `textBoxes`
|
|
928
|
+
* joined with the DTO-derived font + CSS into render-ready {@link TextItem}s. This
|
|
929
|
+
* is the text analogue of the core's `scene()` "paint" for shapes — it lives in
|
|
930
|
+
* the plugin (not the portable core) because the font→CSS stack mapping and the
|
|
931
|
+
* engine `Color`→CSS seam are web concerns, shared across every web framework.
|
|
932
|
+
*/
|
|
933
|
+
/** Map a free-text `/DA` font to a CSS font-family. A standard PDF font → a web
|
|
934
|
+
* stack with the same metrics; anything else is a registered font key → mount it
|
|
935
|
+
* as a `@font-face` of that family (see the framework `mountWebFont` helper). */
|
|
936
|
+
const STANDARD_FONT_CSS = {
|
|
937
|
+
helvetica: "Helvetica, Arial, sans-serif",
|
|
938
|
+
"helvetica-bold": "Helvetica, Arial, sans-serif",
|
|
939
|
+
courier: "\"Courier New\", Courier, monospace",
|
|
940
|
+
"times-roman": "\"Times New Roman\", Times, serif",
|
|
941
|
+
symbol: "serif",
|
|
942
|
+
"zapf-dingbats": "serif"
|
|
943
|
+
};
|
|
944
|
+
const cssFontFor = (font) => STANDARD_FONT_CSS[font] ?? `"${font}", sans-serif`;
|
|
945
|
+
/** Project the model's free-text boxes into render-ready {@link TextItem}s — the
|
|
946
|
+
* core geometry (`textBoxes`) joined with the DTO-derived CSS. Pure; memoized by
|
|
947
|
+
* model identity at the call site so selectors get a stable reference. */
|
|
948
|
+
function buildTextItems(m, pon, view) {
|
|
949
|
+
return textBoxes(m, pon, view).map((tb) => {
|
|
950
|
+
const a = m.byId[tb.id];
|
|
951
|
+
const t = a?.text ?? initialTextStyle;
|
|
952
|
+
return {
|
|
953
|
+
id: tb.id,
|
|
954
|
+
ref: a?.ref ?? null,
|
|
955
|
+
box: tb.box,
|
|
956
|
+
contents: a?.data?.contents ?? "",
|
|
957
|
+
editing: tb.editing,
|
|
958
|
+
...tb.rot ? { rot: tb.rot } : {},
|
|
959
|
+
css: {
|
|
960
|
+
fontFamily: cssFontFor(t.fontFamily),
|
|
961
|
+
fontSize: t.fontSize,
|
|
962
|
+
lineHeight: t.fontSize,
|
|
963
|
+
color: t.fontColor,
|
|
964
|
+
align: t.textAlign,
|
|
965
|
+
padding: 2,
|
|
966
|
+
background: a?.style.interiorColor ?? null
|
|
967
|
+
}
|
|
968
|
+
};
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
//#endregion
|
|
972
|
+
//#region src/tools.ts
|
|
973
|
+
/**
|
|
974
|
+
* The creation properties each known authoring kind actually consumes. This is
|
|
975
|
+
* deliberately separate from the flat internal patch vocabulary: it gives tool
|
|
976
|
+
* configuration precise compile-time and runtime validation while mixed-selection
|
|
977
|
+
* edits can keep using `AnnotationPropsPatch`.
|
|
978
|
+
*/
|
|
979
|
+
const TOOL_DEFAULT_KEYS = {
|
|
980
|
+
square: [
|
|
981
|
+
"color",
|
|
982
|
+
"interiorColor",
|
|
983
|
+
"opacity",
|
|
984
|
+
"strokeWidth",
|
|
985
|
+
"border"
|
|
986
|
+
],
|
|
987
|
+
circle: [
|
|
988
|
+
"color",
|
|
989
|
+
"interiorColor",
|
|
990
|
+
"opacity",
|
|
991
|
+
"strokeWidth",
|
|
992
|
+
"border"
|
|
993
|
+
],
|
|
994
|
+
line: [
|
|
995
|
+
"color",
|
|
996
|
+
"interiorColor",
|
|
997
|
+
"opacity",
|
|
998
|
+
"strokeWidth",
|
|
999
|
+
"border",
|
|
1000
|
+
"lineEndings"
|
|
1001
|
+
],
|
|
1002
|
+
polygon: [
|
|
1003
|
+
"color",
|
|
1004
|
+
"interiorColor",
|
|
1005
|
+
"opacity",
|
|
1006
|
+
"strokeWidth",
|
|
1007
|
+
"border"
|
|
1008
|
+
],
|
|
1009
|
+
polyline: [
|
|
1010
|
+
"color",
|
|
1011
|
+
"interiorColor",
|
|
1012
|
+
"opacity",
|
|
1013
|
+
"strokeWidth",
|
|
1014
|
+
"border",
|
|
1015
|
+
"lineEndings"
|
|
1016
|
+
],
|
|
1017
|
+
ink: [
|
|
1018
|
+
"color",
|
|
1019
|
+
"opacity",
|
|
1020
|
+
"strokeWidth",
|
|
1021
|
+
"blendMode"
|
|
1022
|
+
],
|
|
1023
|
+
"free-text": [
|
|
1024
|
+
"fontFamily",
|
|
1025
|
+
"fontSize",
|
|
1026
|
+
"fontColor",
|
|
1027
|
+
"textAlign",
|
|
1028
|
+
"opacity",
|
|
1029
|
+
"interiorColor",
|
|
1030
|
+
"color",
|
|
1031
|
+
"strokeWidth",
|
|
1032
|
+
"border"
|
|
1033
|
+
],
|
|
1034
|
+
"free-text-callout": [
|
|
1035
|
+
"fontFamily",
|
|
1036
|
+
"fontSize",
|
|
1037
|
+
"fontColor",
|
|
1038
|
+
"textAlign",
|
|
1039
|
+
"opacity",
|
|
1040
|
+
"interiorColor",
|
|
1041
|
+
"color",
|
|
1042
|
+
"strokeWidth",
|
|
1043
|
+
"border",
|
|
1044
|
+
"lineEndings"
|
|
1045
|
+
],
|
|
1046
|
+
highlight: [
|
|
1047
|
+
"color",
|
|
1048
|
+
"opacity",
|
|
1049
|
+
"blendMode"
|
|
1050
|
+
],
|
|
1051
|
+
underline: [
|
|
1052
|
+
"color",
|
|
1053
|
+
"opacity",
|
|
1054
|
+
"blendMode"
|
|
1055
|
+
],
|
|
1056
|
+
strikeout: [
|
|
1057
|
+
"color",
|
|
1058
|
+
"opacity",
|
|
1059
|
+
"blendMode"
|
|
1060
|
+
],
|
|
1061
|
+
squiggly: [
|
|
1062
|
+
"color",
|
|
1063
|
+
"opacity",
|
|
1064
|
+
"blendMode"
|
|
1065
|
+
],
|
|
1066
|
+
caret: ["color", "opacity"],
|
|
1067
|
+
redact: [
|
|
1068
|
+
"color",
|
|
1069
|
+
"interiorColor",
|
|
1070
|
+
"opacity",
|
|
1071
|
+
"fontFamily",
|
|
1072
|
+
"fontSize",
|
|
1073
|
+
"fontColor",
|
|
1074
|
+
"textAlign"
|
|
1075
|
+
],
|
|
1076
|
+
stamp: [],
|
|
1077
|
+
link: ["link"],
|
|
1078
|
+
text: [
|
|
1079
|
+
"icon",
|
|
1080
|
+
"color",
|
|
1081
|
+
"opacity"
|
|
1082
|
+
],
|
|
1083
|
+
"file-attachment": [
|
|
1084
|
+
"icon",
|
|
1085
|
+
"color",
|
|
1086
|
+
"opacity"
|
|
1087
|
+
],
|
|
1088
|
+
"widget-text": [
|
|
1089
|
+
"color",
|
|
1090
|
+
"interiorColor",
|
|
1091
|
+
"strokeWidth",
|
|
1092
|
+
"border",
|
|
1093
|
+
"fontFamily",
|
|
1094
|
+
"fontSize",
|
|
1095
|
+
"fontColor",
|
|
1096
|
+
"textAlign"
|
|
1097
|
+
],
|
|
1098
|
+
"widget-choice": [
|
|
1099
|
+
"color",
|
|
1100
|
+
"interiorColor",
|
|
1101
|
+
"strokeWidth",
|
|
1102
|
+
"border",
|
|
1103
|
+
"fontFamily",
|
|
1104
|
+
"fontSize",
|
|
1105
|
+
"fontColor",
|
|
1106
|
+
"textAlign"
|
|
1107
|
+
],
|
|
1108
|
+
"widget-toggle": [
|
|
1109
|
+
"color",
|
|
1110
|
+
"interiorColor",
|
|
1111
|
+
"strokeWidth",
|
|
1112
|
+
"border"
|
|
1113
|
+
]
|
|
1114
|
+
};
|
|
1115
|
+
const DRAW_TAGS = ["annotation-draw", "annotation-edit"];
|
|
1116
|
+
const MARKUP_TAGS = ["text-select", "annotation-edit"];
|
|
1117
|
+
/**
|
|
1118
|
+
* The built-in tools — a data mirror of v2's registrations (shapes + lines + ink
|
|
1119
|
+
* + free text in the draw channel, text markup + caret behind text selection, and
|
|
1120
|
+
* the click-to-place stamp). Order is display-neutral; the toolbar owns layout.
|
|
1121
|
+
*/
|
|
1122
|
+
const DEFAULT_TOOLS = [
|
|
1123
|
+
{
|
|
1124
|
+
id: "square",
|
|
1125
|
+
subtype: "square",
|
|
1126
|
+
cursor: "crosshair",
|
|
1127
|
+
enables: DRAW_TAGS,
|
|
1128
|
+
defaults: { strokeWidth: 6 },
|
|
1129
|
+
clickCreate: {
|
|
1130
|
+
width: 80,
|
|
1131
|
+
height: 60
|
|
1132
|
+
}
|
|
1133
|
+
},
|
|
1134
|
+
{
|
|
1135
|
+
id: "circle",
|
|
1136
|
+
subtype: "circle",
|
|
1137
|
+
cursor: "crosshair",
|
|
1138
|
+
enables: DRAW_TAGS,
|
|
1139
|
+
defaults: { strokeWidth: 6 },
|
|
1140
|
+
clickCreate: {
|
|
1141
|
+
width: 80,
|
|
1142
|
+
height: 60
|
|
1143
|
+
}
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
id: "line",
|
|
1147
|
+
subtype: "line",
|
|
1148
|
+
cursor: "crosshair",
|
|
1149
|
+
enables: DRAW_TAGS,
|
|
1150
|
+
defaults: { strokeWidth: 6 },
|
|
1151
|
+
clickCreate: { length: 80 }
|
|
1152
|
+
},
|
|
1153
|
+
{
|
|
1154
|
+
id: "polygon",
|
|
1155
|
+
subtype: "polygon",
|
|
1156
|
+
cursor: "crosshair",
|
|
1157
|
+
enables: DRAW_TAGS,
|
|
1158
|
+
defaults: { strokeWidth: 6 }
|
|
1159
|
+
},
|
|
1160
|
+
{
|
|
1161
|
+
id: "polyline",
|
|
1162
|
+
subtype: "polyline",
|
|
1163
|
+
cursor: "crosshair",
|
|
1164
|
+
enables: DRAW_TAGS,
|
|
1165
|
+
defaults: { strokeWidth: 6 }
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
id: "ink",
|
|
1169
|
+
subtype: "ink",
|
|
1170
|
+
cursor: "crosshair",
|
|
1171
|
+
enables: DRAW_TAGS,
|
|
1172
|
+
defaults: {
|
|
1173
|
+
color: "#ef4444",
|
|
1174
|
+
strokeWidth: 6,
|
|
1175
|
+
blendMode: "normal"
|
|
1176
|
+
},
|
|
1177
|
+
ink: { groupStrokesMs: 800 }
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
id: "ink-highlight",
|
|
1181
|
+
extends: "ink",
|
|
1182
|
+
intent: "ink-highlight",
|
|
1183
|
+
defaults: {
|
|
1184
|
+
color: "#ffcd45",
|
|
1185
|
+
strokeWidth: 14,
|
|
1186
|
+
blendMode: "multiply"
|
|
1187
|
+
},
|
|
1188
|
+
ink: { straighten: {
|
|
1189
|
+
deviationThreshold: .15,
|
|
1190
|
+
axisSnapDegrees: 15
|
|
1191
|
+
} }
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
id: "free-text",
|
|
1195
|
+
subtype: "free-text",
|
|
1196
|
+
cursor: "crosshair",
|
|
1197
|
+
enables: DRAW_TAGS,
|
|
1198
|
+
defaults: { fontColor: "#ef4444" },
|
|
1199
|
+
upright: true,
|
|
1200
|
+
clickCreate: {
|
|
1201
|
+
width: 180,
|
|
1202
|
+
height: 40,
|
|
1203
|
+
anchor: "top-left"
|
|
1204
|
+
}
|
|
1205
|
+
},
|
|
1206
|
+
{
|
|
1207
|
+
id: "free-text-callout",
|
|
1208
|
+
subtype: "free-text-callout",
|
|
1209
|
+
propsKind: "free-text",
|
|
1210
|
+
cursor: "crosshair",
|
|
1211
|
+
enables: DRAW_TAGS,
|
|
1212
|
+
defaults: {
|
|
1213
|
+
strokeWidth: 6,
|
|
1214
|
+
lineEndings: { end: "open-arrow" }
|
|
1215
|
+
},
|
|
1216
|
+
upright: true
|
|
1217
|
+
},
|
|
1218
|
+
{
|
|
1219
|
+
id: "highlight",
|
|
1220
|
+
subtype: "highlight",
|
|
1221
|
+
cursor: "default",
|
|
1222
|
+
enables: MARKUP_TAGS,
|
|
1223
|
+
defaults: {
|
|
1224
|
+
color: "#ffe16a",
|
|
1225
|
+
blendMode: "multiply"
|
|
1226
|
+
},
|
|
1227
|
+
selection: { kind: "markup" }
|
|
1228
|
+
},
|
|
1229
|
+
{
|
|
1230
|
+
id: "redact",
|
|
1231
|
+
subtype: "redact",
|
|
1232
|
+
cursor: "crosshair",
|
|
1233
|
+
enables: [
|
|
1234
|
+
"text-select",
|
|
1235
|
+
"annotation-draw",
|
|
1236
|
+
"annotation-edit"
|
|
1237
|
+
],
|
|
1238
|
+
defaults: {
|
|
1239
|
+
color: "#e44234",
|
|
1240
|
+
interiorColor: "#000000",
|
|
1241
|
+
fontColor: "#ffffff",
|
|
1242
|
+
opacity: 1
|
|
1243
|
+
},
|
|
1244
|
+
selection: { kind: "markup" }
|
|
1245
|
+
},
|
|
1246
|
+
{
|
|
1247
|
+
id: "underline",
|
|
1248
|
+
subtype: "underline",
|
|
1249
|
+
cursor: "default",
|
|
1250
|
+
enables: MARKUP_TAGS,
|
|
1251
|
+
defaults: { color: "#ef4444" },
|
|
1252
|
+
selection: { kind: "markup" }
|
|
1253
|
+
},
|
|
1254
|
+
{
|
|
1255
|
+
id: "strikeout",
|
|
1256
|
+
subtype: "strikeout",
|
|
1257
|
+
cursor: "default",
|
|
1258
|
+
enables: MARKUP_TAGS,
|
|
1259
|
+
defaults: { color: "#ef4444" },
|
|
1260
|
+
selection: { kind: "markup" }
|
|
1261
|
+
},
|
|
1262
|
+
{
|
|
1263
|
+
id: "squiggly",
|
|
1264
|
+
subtype: "squiggly",
|
|
1265
|
+
cursor: "default",
|
|
1266
|
+
enables: MARKUP_TAGS,
|
|
1267
|
+
defaults: { color: "#ef4444" },
|
|
1268
|
+
selection: { kind: "markup" }
|
|
1269
|
+
},
|
|
1270
|
+
{
|
|
1271
|
+
id: "insert-text",
|
|
1272
|
+
subtype: "caret",
|
|
1273
|
+
propsKind: "caret",
|
|
1274
|
+
preset: "caret",
|
|
1275
|
+
cursor: "default",
|
|
1276
|
+
enables: MARKUP_TAGS,
|
|
1277
|
+
defaults: { color: "#ef4444" },
|
|
1278
|
+
selection: {
|
|
1279
|
+
kind: "text-edit",
|
|
1280
|
+
operation: "insert"
|
|
1281
|
+
}
|
|
1282
|
+
},
|
|
1283
|
+
{
|
|
1284
|
+
id: "replace-text",
|
|
1285
|
+
subtype: "strikeout",
|
|
1286
|
+
propsKind: "strikeout",
|
|
1287
|
+
cursor: "default",
|
|
1288
|
+
enables: MARKUP_TAGS,
|
|
1289
|
+
defaults: { color: "#ef4444" },
|
|
1290
|
+
selection: {
|
|
1291
|
+
kind: "text-edit",
|
|
1292
|
+
operation: "replace"
|
|
1293
|
+
}
|
|
1294
|
+
},
|
|
1295
|
+
{
|
|
1296
|
+
id: "stamp",
|
|
1297
|
+
subtype: "stamp",
|
|
1298
|
+
cursor: "copy",
|
|
1299
|
+
enables: ["annotation-place", "annotation-edit"],
|
|
1300
|
+
source: {
|
|
1301
|
+
kind: "prompt",
|
|
1302
|
+
accept: "image/png,image/jpeg,application/pdf"
|
|
1303
|
+
},
|
|
1304
|
+
upright: true,
|
|
1305
|
+
ghost: { mode: "footprint" }
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
id: "link",
|
|
1309
|
+
subtype: "link",
|
|
1310
|
+
cursor: "crosshair",
|
|
1311
|
+
enables: DRAW_TAGS
|
|
1312
|
+
},
|
|
1313
|
+
{
|
|
1314
|
+
id: "note",
|
|
1315
|
+
subtype: "text",
|
|
1316
|
+
cursor: "crosshair",
|
|
1317
|
+
enables: ["annotation-place", "annotation-edit"],
|
|
1318
|
+
defaults: {
|
|
1319
|
+
icon: "comment",
|
|
1320
|
+
color: "#facc15"
|
|
1321
|
+
},
|
|
1322
|
+
flags: {
|
|
1323
|
+
noZoom: true,
|
|
1324
|
+
noRotate: true
|
|
1325
|
+
},
|
|
1326
|
+
upright: true,
|
|
1327
|
+
ghost: { mode: "footprint" }
|
|
1328
|
+
},
|
|
1329
|
+
{
|
|
1330
|
+
id: "attachment",
|
|
1331
|
+
subtype: "file-attachment",
|
|
1332
|
+
cursor: "copy",
|
|
1333
|
+
enables: ["annotation-place", "annotation-edit"],
|
|
1334
|
+
source: { kind: "prompt" },
|
|
1335
|
+
defaults: {
|
|
1336
|
+
icon: "paperclip",
|
|
1337
|
+
color: "#facc15"
|
|
1338
|
+
},
|
|
1339
|
+
flags: {
|
|
1340
|
+
noZoom: true,
|
|
1341
|
+
noRotate: true
|
|
1342
|
+
},
|
|
1343
|
+
upright: true,
|
|
1344
|
+
ghost: { mode: "footprint" }
|
|
1345
|
+
}
|
|
1346
|
+
];
|
|
1347
|
+
/** Merge two default patches, `b` over `a`, with line endings merged per side. */
|
|
1348
|
+
function mergeDefaults(a, b) {
|
|
1349
|
+
if (!a) return b;
|
|
1350
|
+
if (!b) return a;
|
|
1351
|
+
const merged = {
|
|
1352
|
+
...a,
|
|
1353
|
+
...b
|
|
1354
|
+
};
|
|
1355
|
+
if (a.lineEndings || b.lineEndings) merged.lineEndings = {
|
|
1356
|
+
...a.lineEndings,
|
|
1357
|
+
...b.lineEndings
|
|
1358
|
+
};
|
|
1359
|
+
return merged;
|
|
1360
|
+
}
|
|
1361
|
+
/** Overlay a same-id override onto a base definition (configure a built-in). */
|
|
1362
|
+
function mergeDef(base, over) {
|
|
1363
|
+
return {
|
|
1364
|
+
...base,
|
|
1365
|
+
...over,
|
|
1366
|
+
enables: over.enables ?? base.enables,
|
|
1367
|
+
meta: base.meta || over.meta ? {
|
|
1368
|
+
...base.meta,
|
|
1369
|
+
...over.meta
|
|
1370
|
+
} : void 0,
|
|
1371
|
+
defaults: mergeDefaults(base.defaults, over.defaults),
|
|
1372
|
+
flags: base.flags || over.flags ? {
|
|
1373
|
+
...base.flags,
|
|
1374
|
+
...over.flags
|
|
1375
|
+
} : void 0,
|
|
1376
|
+
ink: base.ink || over.ink ? {
|
|
1377
|
+
...base.ink,
|
|
1378
|
+
...over.ink
|
|
1379
|
+
} : void 0
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
function validateDefaults(tool) {
|
|
1383
|
+
if (!tool.defaults) return;
|
|
1384
|
+
const allowed = TOOL_DEFAULT_KEYS[tool.subtype];
|
|
1385
|
+
if (!allowed) return;
|
|
1386
|
+
const keys = new Set(allowed);
|
|
1387
|
+
for (const key of Object.keys(tool.defaults)) if (!keys.has(key)) throw new Error(`[annotation] tool '${tool.id}' does not support default '${key}' for '${tool.subtype}'`);
|
|
1388
|
+
}
|
|
1389
|
+
/**
|
|
1390
|
+
* Resolve {@link DEFAULT_TOOLS} + embedder overrides into the ready-to-use table.
|
|
1391
|
+
* A same-id override merges over the built-in; a new id adds a tool; `extends`
|
|
1392
|
+
* inherits from an already-defined tool. Keyed by id, so lookups are O(1).
|
|
1393
|
+
*/
|
|
1394
|
+
function buildToolRegistry(overrides = []) {
|
|
1395
|
+
const defs = /* @__PURE__ */ new Map();
|
|
1396
|
+
for (const d of DEFAULT_TOOLS) defs.set(d.id, d);
|
|
1397
|
+
for (const o of overrides) {
|
|
1398
|
+
const prev = defs.get(o.id);
|
|
1399
|
+
defs.set(o.id, prev ? mergeDef(prev, o) : o);
|
|
1400
|
+
}
|
|
1401
|
+
const out = /* @__PURE__ */ new Map();
|
|
1402
|
+
const resolving = /* @__PURE__ */ new Set();
|
|
1403
|
+
const resolve = (id) => {
|
|
1404
|
+
const cached = out.get(id);
|
|
1405
|
+
if (cached) return cached;
|
|
1406
|
+
const def = defs.get(id);
|
|
1407
|
+
if (!def) throw new Error(`[annotation] tool '${id}' extends an unknown tool`);
|
|
1408
|
+
let base;
|
|
1409
|
+
if (def.extends && def.extends !== id && !resolving.has(def.extends)) {
|
|
1410
|
+
resolving.add(id);
|
|
1411
|
+
base = resolve(def.extends);
|
|
1412
|
+
resolving.delete(id);
|
|
1413
|
+
}
|
|
1414
|
+
const subtype = def.subtype ?? base?.subtype ?? def.id;
|
|
1415
|
+
const resolved = {
|
|
1416
|
+
id: def.id,
|
|
1417
|
+
subtype,
|
|
1418
|
+
preset: def.preset ?? def.id,
|
|
1419
|
+
propsKind: def.propsKind ?? base?.propsKind ?? subtype,
|
|
1420
|
+
cursor: def.cursor ?? base?.cursor ?? "crosshair",
|
|
1421
|
+
enables: new Set(def.enables ?? (base ? [...base.enables] : [])),
|
|
1422
|
+
defaults: mergeDefaults(base?.defaults, def.defaults),
|
|
1423
|
+
flags: base?.flags || def.flags ? {
|
|
1424
|
+
...base?.flags,
|
|
1425
|
+
...def.flags
|
|
1426
|
+
} : void 0,
|
|
1427
|
+
source: def.source ?? base?.source,
|
|
1428
|
+
selection: def.selection ?? base?.selection,
|
|
1429
|
+
intent: def.intent ?? base?.intent,
|
|
1430
|
+
ink: base?.ink || def.ink ? {
|
|
1431
|
+
...base?.ink,
|
|
1432
|
+
...def.ink
|
|
1433
|
+
} : void 0,
|
|
1434
|
+
upright: def.upright ?? base?.upright ?? false,
|
|
1435
|
+
clickCreate: def.clickCreate ?? base?.clickCreate ?? false,
|
|
1436
|
+
ghost: def.ghost ?? base?.ghost ?? false,
|
|
1437
|
+
meta: base?.meta || def.meta ? {
|
|
1438
|
+
...base?.meta,
|
|
1439
|
+
...def.meta
|
|
1440
|
+
} : void 0
|
|
1441
|
+
};
|
|
1442
|
+
validateDefaults(resolved);
|
|
1443
|
+
out.set(id, resolved);
|
|
1444
|
+
return resolved;
|
|
1445
|
+
};
|
|
1446
|
+
for (const id of defs.keys()) resolve(id);
|
|
1447
|
+
return out;
|
|
1448
|
+
}
|
|
1449
|
+
//#endregion
|
|
1450
|
+
//#region src/placement.ts
|
|
1451
|
+
/**
|
|
1452
|
+
* Per-kind CODE for the click-to-place ICON kinds (note / file attachment)
|
|
1453
|
+
* — the `props.ts`/`repository.ts` pattern: all tool CONFIG stays in the
|
|
1454
|
+
* tool table; this module only interprets it. The stamp keeps its own
|
|
1455
|
+
* sizing/sniffing path in the capability; everything funnels through the
|
|
1456
|
+
* one `placeAt` entry there.
|
|
1457
|
+
*/
|
|
1458
|
+
/**
|
|
1459
|
+
* Icon kinds place at the generator's fixed box: the engine forces the
|
|
1460
|
+
* `/Rect` to 20×20 from its bottom-left corner (the spec's note-icon rule),
|
|
1461
|
+
* so the footprint ghost and the placement use exactly this size.
|
|
1462
|
+
*/
|
|
1463
|
+
const ICON_PLACE_SIZE = {
|
|
1464
|
+
width: 20,
|
|
1465
|
+
height: 20
|
|
1466
|
+
};
|
|
1467
|
+
const isIconPlaceKind = (subtype) => subtype === "text" || subtype === "file-attachment";
|
|
1468
|
+
/**
|
|
1469
|
+
* Build the engine create draft for a placed icon annotation. `geom` is the
|
|
1470
|
+
* repository's `boxGeomFields` emit (the `/Rect` + upright rotation pair);
|
|
1471
|
+
* `defaults` is the tool's resolved flat props bag (`defaultsFor`) — the
|
|
1472
|
+
* colour seam is crossed here via the repository's `cssToColor`, and the
|
|
1473
|
+
* icon falls back to the kind's own default when the bag carries none.
|
|
1474
|
+
*/
|
|
1475
|
+
function iconPlacementDraft(subtype, geom, defaults, flags, file) {
|
|
1476
|
+
const shared = {
|
|
1477
|
+
...geom,
|
|
1478
|
+
color: cssToColor(defaults.color),
|
|
1479
|
+
opacity: defaults.opacity,
|
|
1480
|
+
flags: {
|
|
1481
|
+
print: true,
|
|
1482
|
+
...flags
|
|
1483
|
+
}
|
|
1484
|
+
};
|
|
1485
|
+
if (subtype === "text") return {
|
|
1486
|
+
subtype: "text",
|
|
1487
|
+
icon: defaults.icon ?? "comment",
|
|
1488
|
+
...shared
|
|
1489
|
+
};
|
|
1490
|
+
if (!file) throw new Error("[annotation] a file-attachment placement requires a file payload");
|
|
1491
|
+
return {
|
|
1492
|
+
subtype: "file-attachment",
|
|
1493
|
+
icon: defaults.icon ?? "paperclip",
|
|
1494
|
+
file,
|
|
1495
|
+
...shared
|
|
1496
|
+
};
|
|
1497
|
+
}
|
|
1498
|
+
//#endregion
|
|
1499
|
+
//#region src/capability.ts
|
|
1500
|
+
/** Fold `zoom`/`rotation` host args into the core's ViewEnv (or none).
|
|
1501
|
+
* `zoom` is the page's RELATIVE zoom (`transform.zoom`) — never the
|
|
1502
|
+
* px-per-point `scale` (that one only converts CSS-px chrome settings). */
|
|
1503
|
+
const viewEnv = (zoom, rotation) => zoom != null || rotation != null ? {
|
|
1504
|
+
zoom: zoom ?? 1,
|
|
1505
|
+
rotation: rotation ?? 0
|
|
1506
|
+
} : void 0;
|
|
1507
|
+
/** Broad annotate-write capability (PDF bit 6). The engine independently
|
|
1508
|
+
* enforces this AND the per-owner collab rules; `canEdit`/`canDelete` are the
|
|
1509
|
+
* UI mirror of the coarse gate. */
|
|
1510
|
+
const ANNOTATE_MODIFY = "doc.annotate.modify";
|
|
1511
|
+
const TEXT_COMMIT_DEBOUNCE_MS = 250;
|
|
1512
|
+
/**
|
|
1513
|
+
* The annotation shell. The pure `update` runs HERE (so it can emit effects);
|
|
1514
|
+
* the resulting model is dispatched to the store, and each effect is performed
|
|
1515
|
+
* against the engine repository — optimistic create → reconcile to the durable
|
|
1516
|
+
* ref, patch/delete fire-and-forget.
|
|
1517
|
+
*/
|
|
1518
|
+
function createAnnotationCapability(ctx, config = {}) {
|
|
1519
|
+
const loaded = /* @__PURE__ */ new Set();
|
|
1520
|
+
const behaviors = [];
|
|
1521
|
+
/** Per-annotation debounce timer for the engine `contents` write while typing. */
|
|
1522
|
+
const textTimers = /* @__PURE__ */ new Map();
|
|
1523
|
+
const configTools = config.tools ?? [];
|
|
1524
|
+
const registry = buildToolRegistry(configTools);
|
|
1525
|
+
/** The installed file-picker port (a DOM file dialog, wired by the framework
|
|
1526
|
+
* adapter), or null — every click-then-pick tool resolves through this ONE
|
|
1527
|
+
* slot. See {@link FilePickerProvider}. */
|
|
1528
|
+
let filePickerProvider = null;
|
|
1529
|
+
const model = () => ctx.getState().model;
|
|
1530
|
+
const chromeSettings = () => ctx.getState().chrome;
|
|
1531
|
+
/**
|
|
1532
|
+
* Ids on a page whose Behavior is currently ENGAGED (form widgets under a
|
|
1533
|
+
* fill tool): they render their own DOM, so hit-test/marquee must not see
|
|
1534
|
+
* them. Resolved per event — engagement follows the active tool live.
|
|
1535
|
+
*/
|
|
1536
|
+
const inertIdsAt = (pon) => {
|
|
1537
|
+
if (!behaviors.length) return void 0;
|
|
1538
|
+
const m = model();
|
|
1539
|
+
let out;
|
|
1540
|
+
for (const id of m.order) {
|
|
1541
|
+
const a = m.byId[id];
|
|
1542
|
+
if (!a || a.pon !== pon) continue;
|
|
1543
|
+
if (behaviors.some((b) => b.matches({
|
|
1544
|
+
subtype: a.subtype,
|
|
1545
|
+
ref: a.ref
|
|
1546
|
+
}) && b.engaged())) (out ?? (out = /* @__PURE__ */ new Set())).add(id);
|
|
1547
|
+
}
|
|
1548
|
+
return out;
|
|
1549
|
+
};
|
|
1550
|
+
/** The CSS-px chrome settings converted to CONTENT units by the page's view
|
|
1551
|
+
* scale (px per content unit) — screen-constant grab zones + stalk at every
|
|
1552
|
+
* zoom. No scale → the values are read as content units (headless callers). */
|
|
1553
|
+
const chromeGeomAt = (scale) => {
|
|
1554
|
+
const cs = chromeSettings();
|
|
1555
|
+
const s = scale || 1;
|
|
1556
|
+
return {
|
|
1557
|
+
handleTol: cs.handles.hitSize / 2 / s,
|
|
1558
|
+
knobTol: cs.knob.hitSize / 2 / s,
|
|
1559
|
+
knobOffset: cs.knob.offset / s
|
|
1560
|
+
};
|
|
1561
|
+
};
|
|
1562
|
+
const itemsCache = /* @__PURE__ */ new Map();
|
|
1563
|
+
const chromeCache = /* @__PURE__ */ new Map();
|
|
1564
|
+
const memoItems = (pon, view) => {
|
|
1565
|
+
const m = model();
|
|
1566
|
+
const g = ctx.getState().toolGhost;
|
|
1567
|
+
const c = itemsCache.get(pon);
|
|
1568
|
+
if (c && c.model === m && c.ghost === g && c.zoom === view?.zoom && c.rotation === view?.rotation) return c.v;
|
|
1569
|
+
const v = pageItems(m, pon, view);
|
|
1570
|
+
if (g && g.pon === pon && g.kind === "vector") {
|
|
1571
|
+
const tool = registry.get(g.toolId);
|
|
1572
|
+
const style = styleFromProps(defaultsFor(m, tool?.preset ?? g.toolId));
|
|
1573
|
+
v.push({
|
|
1574
|
+
id: "tool-ghost",
|
|
1575
|
+
ref: null,
|
|
1576
|
+
subtype: tool?.subtype ?? "square",
|
|
1577
|
+
geom: g.geom,
|
|
1578
|
+
box: g.box,
|
|
1579
|
+
style,
|
|
1580
|
+
source: "ghost",
|
|
1581
|
+
selected: false
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
itemsCache.set(pon, {
|
|
1585
|
+
model: m,
|
|
1586
|
+
ghost: g,
|
|
1587
|
+
zoom: view?.zoom,
|
|
1588
|
+
rotation: view?.rotation,
|
|
1589
|
+
v
|
|
1590
|
+
});
|
|
1591
|
+
return v;
|
|
1592
|
+
};
|
|
1593
|
+
const memoChrome = (pon, scale, rotation, zoom) => {
|
|
1594
|
+
const m = model();
|
|
1595
|
+
const cs = chromeSettings();
|
|
1596
|
+
const c = chromeCache.get(pon);
|
|
1597
|
+
if (c && c.model === m && c.cs === cs && c.scale === scale && c.rotation === rotation && c.zoom === zoom) return c.v;
|
|
1598
|
+
let v = chrome(m, pon, pageBoxOf(pon), chromeGeomAt(scale).knobOffset, viewEnv(zoom, rotation));
|
|
1599
|
+
if (!cs.guides.enabled) v = v.filter((n) => n.kind !== "rotate-guides");
|
|
1600
|
+
chromeCache.set(pon, {
|
|
1601
|
+
model: m,
|
|
1602
|
+
cs,
|
|
1603
|
+
scale,
|
|
1604
|
+
rotation,
|
|
1605
|
+
zoom,
|
|
1606
|
+
v
|
|
1607
|
+
});
|
|
1608
|
+
return v;
|
|
1609
|
+
};
|
|
1610
|
+
let anchorCache = null;
|
|
1611
|
+
const memoAnchor = (scale, rotation, zoom) => {
|
|
1612
|
+
const m = model();
|
|
1613
|
+
const cs = chromeSettings();
|
|
1614
|
+
if (anchorCache && anchorCache.model === m && anchorCache.cs === cs && anchorCache.scale === scale && anchorCache.rotation === rotation && anchorCache.zoom === zoom) return anchorCache.v;
|
|
1615
|
+
const v = selectionAnchor(m, pageBoxOf, () => chromeGeomAt(scale).knobOffset, () => viewEnv(zoom, rotation));
|
|
1616
|
+
anchorCache = {
|
|
1617
|
+
model: m,
|
|
1618
|
+
cs,
|
|
1619
|
+
scale,
|
|
1620
|
+
rotation,
|
|
1621
|
+
zoom,
|
|
1622
|
+
v
|
|
1623
|
+
};
|
|
1624
|
+
return v;
|
|
1625
|
+
};
|
|
1626
|
+
let draftAnchorCache = null;
|
|
1627
|
+
const memoDraftAnchor = () => {
|
|
1628
|
+
const m = model();
|
|
1629
|
+
if (draftAnchorCache && draftAnchorCache.model === m) return draftAnchorCache.v;
|
|
1630
|
+
const v = creationDraftAnchor(m);
|
|
1631
|
+
draftAnchorCache = {
|
|
1632
|
+
model: m,
|
|
1633
|
+
v
|
|
1634
|
+
};
|
|
1635
|
+
return v;
|
|
1636
|
+
};
|
|
1637
|
+
let selPropsCache = null;
|
|
1638
|
+
const memoSelectionProps = () => {
|
|
1639
|
+
const m = model();
|
|
1640
|
+
if (selPropsCache && selPropsCache.model === m) return selPropsCache.v;
|
|
1641
|
+
const members = m.selected.map((id) => m.byId[id]).filter((a) => !!a);
|
|
1642
|
+
const specs = sharedProps(members.map((a) => a.subtype));
|
|
1643
|
+
const values = {};
|
|
1644
|
+
const mixed = [];
|
|
1645
|
+
for (const spec of specs) {
|
|
1646
|
+
const first = readProp(members[0], spec.key);
|
|
1647
|
+
values[spec.key] = first;
|
|
1648
|
+
const firstJson = JSON.stringify(first);
|
|
1649
|
+
if (members.some((a) => JSON.stringify(readProp(a, spec.key)) !== firstJson)) mixed.push(spec.key);
|
|
1650
|
+
}
|
|
1651
|
+
const v = {
|
|
1652
|
+
specs,
|
|
1653
|
+
values,
|
|
1654
|
+
mixed
|
|
1655
|
+
};
|
|
1656
|
+
selPropsCache = {
|
|
1657
|
+
model: m,
|
|
1658
|
+
v
|
|
1659
|
+
};
|
|
1660
|
+
return v;
|
|
1661
|
+
};
|
|
1662
|
+
const linkItemsCache = /* @__PURE__ */ new Map();
|
|
1663
|
+
const memoLinkItems = (pon) => {
|
|
1664
|
+
const m = model();
|
|
1665
|
+
const c = linkItemsCache.get(pon);
|
|
1666
|
+
if (c && c.model === m) return c.v;
|
|
1667
|
+
const v = [];
|
|
1668
|
+
for (const id of m.order) {
|
|
1669
|
+
const a = m.byId[id];
|
|
1670
|
+
if (!a || a.pon !== pon || a.link == null) continue;
|
|
1671
|
+
if (!viewable(a.flags, false)) continue;
|
|
1672
|
+
if (a.subtype === "link") {
|
|
1673
|
+
if (a.geom.t === "rect") v.push({
|
|
1674
|
+
id,
|
|
1675
|
+
rect: a.geom.rect,
|
|
1676
|
+
target: a.link
|
|
1677
|
+
});
|
|
1678
|
+
} else linkChildRects(a).forEach((rect, i) => v.push({
|
|
1679
|
+
id: `${id}#${i}`,
|
|
1680
|
+
rect,
|
|
1681
|
+
target: a.link
|
|
1682
|
+
}));
|
|
1683
|
+
}
|
|
1684
|
+
linkItemsCache.set(pon, {
|
|
1685
|
+
model: m,
|
|
1686
|
+
v
|
|
1687
|
+
});
|
|
1688
|
+
return v;
|
|
1689
|
+
};
|
|
1690
|
+
const textsCache = /* @__PURE__ */ new Map();
|
|
1691
|
+
const memoTexts = (pon, view) => {
|
|
1692
|
+
const m = model();
|
|
1693
|
+
const c = textsCache.get(pon);
|
|
1694
|
+
if (c && c.model === m && c.zoom === view?.zoom && c.rotation === view?.rotation) return c.v;
|
|
1695
|
+
const v = buildTextItems(m, pon, view);
|
|
1696
|
+
textsCache.set(pon, {
|
|
1697
|
+
model: m,
|
|
1698
|
+
zoom: view?.zoom,
|
|
1699
|
+
rotation: view?.rotation,
|
|
1700
|
+
v
|
|
1701
|
+
});
|
|
1702
|
+
return v;
|
|
1703
|
+
};
|
|
1704
|
+
let selFlagsCache = null;
|
|
1705
|
+
const memoSelectionFlags = () => {
|
|
1706
|
+
const m = model();
|
|
1707
|
+
if (selFlagsCache && selFlagsCache.model === m) return selFlagsCache.v;
|
|
1708
|
+
const members = m.selected.map((id) => m.byId[id]).filter((a) => !!a);
|
|
1709
|
+
let v = null;
|
|
1710
|
+
if (members.length) {
|
|
1711
|
+
v = {};
|
|
1712
|
+
for (const key of FLAG_KEYS) {
|
|
1713
|
+
const first = members[0].flags[key];
|
|
1714
|
+
v[key] = members.every((a) => a.flags[key] === first) ? first : null;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
selFlagsCache = {
|
|
1718
|
+
model: m,
|
|
1719
|
+
v
|
|
1720
|
+
};
|
|
1721
|
+
return v;
|
|
1722
|
+
};
|
|
1723
|
+
const cropOf = (pon) => ctx.document()?.pages.find((p) => p.pageObjectNumber === pon)?.boxes.crop ?? null;
|
|
1724
|
+
/** The page's box in content space (origin at the crop top-left) — the box
|
|
1725
|
+
* pointer gestures clamp to, so annotations stay page-bound. */
|
|
1726
|
+
const pageBoxOf = (pon) => {
|
|
1727
|
+
const crop = cropOf(pon);
|
|
1728
|
+
return crop ? {
|
|
1729
|
+
x: 0,
|
|
1730
|
+
y: 0,
|
|
1731
|
+
width: crop.right - crop.left,
|
|
1732
|
+
height: crop.top - crop.bottom
|
|
1733
|
+
} : void 0;
|
|
1734
|
+
};
|
|
1735
|
+
/**
|
|
1736
|
+
* The armed stamp-tool payload: the bytes the next click places, plus the
|
|
1737
|
+
* PDF-point placement size (derived from the sniffed intrinsic aspect).
|
|
1738
|
+
* Transient tool state — deliberately NOT in the model: it is never
|
|
1739
|
+
* rendered, never synced, and dies with the tool.
|
|
1740
|
+
*/
|
|
1741
|
+
let armedStamp = null;
|
|
1742
|
+
/**
|
|
1743
|
+
* The desired stamp size (PDF points) from sniffed bytes: the image's
|
|
1744
|
+
* INTRINSIC pixel dimensions taken 1:1 as points (the v2 rule — keep the
|
|
1745
|
+
* artwork's own size, then clamp to the page at placement). A `targetWidth`
|
|
1746
|
+
* override scales to that width, aspect preserved. Vector (PDF) stamps carry
|
|
1747
|
+
* no sniffable dimensions — a caller-supplied `intrinsic` override (a stamp
|
|
1748
|
+
* library knows its page size) keeps the true aspect; without one they fall
|
|
1749
|
+
* back to a square target.
|
|
1750
|
+
*/
|
|
1751
|
+
const desiredStampSize = (meta, targetWidth, intrinsicOverride) => {
|
|
1752
|
+
const intrinsic = intrinsicOverride && intrinsicOverride.width > 0 && intrinsicOverride.height > 0 ? intrinsicOverride : "width" in meta && meta.width > 0 ? {
|
|
1753
|
+
width: meta.width,
|
|
1754
|
+
height: meta.height
|
|
1755
|
+
} : {
|
|
1756
|
+
width: targetWidth ?? 150,
|
|
1757
|
+
height: targetWidth ?? 150
|
|
1758
|
+
};
|
|
1759
|
+
if (targetWidth === void 0) return intrinsic;
|
|
1760
|
+
return {
|
|
1761
|
+
width: targetWidth,
|
|
1762
|
+
height: targetWidth * (intrinsic.height / intrinsic.width)
|
|
1763
|
+
};
|
|
1764
|
+
};
|
|
1765
|
+
const armStamp = async (input) => {
|
|
1766
|
+
const resolved = await resolveBinarySource(input.source);
|
|
1767
|
+
const meta = sniffBinaryMetadata(resolved.bytes);
|
|
1768
|
+
if (!meta) throw new Error("[annotation] stamp source must be PNG, JPEG, or single-page PDF bytes");
|
|
1769
|
+
let preview = null;
|
|
1770
|
+
if (input.preview) {
|
|
1771
|
+
const p = await resolveBinarySource(input.preview);
|
|
1772
|
+
preview = {
|
|
1773
|
+
bytes: new Uint8Array(p.bytes),
|
|
1774
|
+
mimeType: p.mimeType
|
|
1775
|
+
};
|
|
1776
|
+
} else if (meta.mimeType !== "application/pdf") preview = {
|
|
1777
|
+
bytes: new Uint8Array(resolved.bytes),
|
|
1778
|
+
mimeType: meta.mimeType
|
|
1779
|
+
};
|
|
1780
|
+
armedStamp = {
|
|
1781
|
+
source: input.source,
|
|
1782
|
+
...desiredStampSize(meta, input.targetWidth, input.intrinsicSize),
|
|
1783
|
+
preview
|
|
1784
|
+
};
|
|
1785
|
+
ctx.dispatch({ type: "STAMP_ARM_CHANGED" });
|
|
1786
|
+
ctx.tryGet(InteractionToken)?.activateTool("stamp");
|
|
1787
|
+
};
|
|
1788
|
+
const disarmStamp = () => {
|
|
1789
|
+
if (!armedStamp) return;
|
|
1790
|
+
armedStamp = null;
|
|
1791
|
+
ctx.dispatch({ type: "STAMP_ARM_CHANGED" });
|
|
1792
|
+
};
|
|
1793
|
+
/** Move the hover FOOTPRINT ghost to a content point. The box/geometry is
|
|
1794
|
+
* computed by the SAME rules the click's placement uses ({@link createStampAt}
|
|
1795
|
+
* fit + clamp for an armed stamp; the click-create anchor + page clamp for a
|
|
1796
|
+
* draw tool), so the ghost is the placement, not an approximation of it. */
|
|
1797
|
+
const ghostHoverAt = (toolId, pon, point, displayRotation) => {
|
|
1798
|
+
const tool = registry.get(toolId);
|
|
1799
|
+
const crop = cropOf(pon);
|
|
1800
|
+
if (!tool || tool.ghost === false || !crop) {
|
|
1801
|
+
clearGhost();
|
|
1802
|
+
return;
|
|
1803
|
+
}
|
|
1804
|
+
const page = {
|
|
1805
|
+
width: crop.right - crop.left,
|
|
1806
|
+
height: crop.top - crop.bottom
|
|
1807
|
+
};
|
|
1808
|
+
if (armedStamp) {
|
|
1809
|
+
const rot = uprightRotFor(displayRotation);
|
|
1810
|
+
const box = fitStampBox(point, {
|
|
1811
|
+
width: armedStamp.width,
|
|
1812
|
+
height: armedStamp.height
|
|
1813
|
+
}, page, rot);
|
|
1814
|
+
ctx.dispatch({
|
|
1815
|
+
type: "SET_TOOL_GHOST",
|
|
1816
|
+
ghost: {
|
|
1817
|
+
pon,
|
|
1818
|
+
box,
|
|
1819
|
+
rot,
|
|
1820
|
+
kind: "image"
|
|
1821
|
+
}
|
|
1822
|
+
});
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
if (isIconPlaceKind(tool.subtype)) {
|
|
1826
|
+
const box = fitStampBox(point, ICON_PLACE_SIZE, page, uprightRotFor(displayRotation));
|
|
1827
|
+
dispatchVectorGhost(pon, toolId, {
|
|
1828
|
+
t: "rect",
|
|
1829
|
+
rect: box,
|
|
1830
|
+
ellipse: false
|
|
1831
|
+
});
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1834
|
+
if (!tool.clickCreate) {
|
|
1835
|
+
clearGhost();
|
|
1836
|
+
return;
|
|
1837
|
+
}
|
|
1838
|
+
const placement = resolveClickPlacement(point, tool.clickCreate, {
|
|
1839
|
+
pageBox: {
|
|
1840
|
+
x: 0,
|
|
1841
|
+
y: 0,
|
|
1842
|
+
width: page.width,
|
|
1843
|
+
height: page.height
|
|
1844
|
+
},
|
|
1845
|
+
upright: tool.upright,
|
|
1846
|
+
displayRotation
|
|
1847
|
+
});
|
|
1848
|
+
const geom = clickCreateGeom(tool.subtype, placement, defaultsFor(model(), tool.preset));
|
|
1849
|
+
if (!geom) {
|
|
1850
|
+
clearGhost();
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
dispatchVectorGhost(pon, toolId, geom);
|
|
1854
|
+
};
|
|
1855
|
+
/** Paint a vector ghost item for a tool's would-be geometry — shared by the
|
|
1856
|
+
* hover footprint and the externally-driven placement preview. */
|
|
1857
|
+
const dispatchVectorGhost = (pon, toolId, geom) => {
|
|
1858
|
+
const tool = registry.get(toolId);
|
|
1859
|
+
const style = styleFromProps(defaultsFor(model(), tool?.preset ?? toolId));
|
|
1860
|
+
ctx.dispatch({
|
|
1861
|
+
type: "SET_TOOL_GHOST",
|
|
1862
|
+
ghost: {
|
|
1863
|
+
pon,
|
|
1864
|
+
box: geomVisualBounds(geom, style.strokeWidth, style.border),
|
|
1865
|
+
rot: 0,
|
|
1866
|
+
kind: "vector",
|
|
1867
|
+
toolId,
|
|
1868
|
+
geom
|
|
1869
|
+
}
|
|
1870
|
+
});
|
|
1871
|
+
};
|
|
1872
|
+
/**
|
|
1873
|
+
* Drive the placement preview during an EXTERNALLY-owned creation gesture
|
|
1874
|
+
* (the form plugin's drag-to-place): paint the box the commit would use,
|
|
1875
|
+
* styled from the TOOL's defaults, through the same ghost pipeline as every
|
|
1876
|
+
* footprint. The box is clamped to the page (a drag may overshoot).
|
|
1877
|
+
*/
|
|
1878
|
+
const setPlacementPreview = (toolId, pon, box) => {
|
|
1879
|
+
const crop = cropOf(pon);
|
|
1880
|
+
if (!crop) return;
|
|
1881
|
+
const page = {
|
|
1882
|
+
width: crop.right - crop.left,
|
|
1883
|
+
height: crop.top - crop.bottom
|
|
1884
|
+
};
|
|
1885
|
+
const x = Math.max(0, Math.min(box.x, page.width));
|
|
1886
|
+
const y = Math.max(0, Math.min(box.y, page.height));
|
|
1887
|
+
const rect = {
|
|
1888
|
+
x,
|
|
1889
|
+
y,
|
|
1890
|
+
width: Math.max(0, Math.min(box.x + box.width, page.width) - x),
|
|
1891
|
+
height: Math.max(0, Math.min(box.y + box.height, page.height) - y)
|
|
1892
|
+
};
|
|
1893
|
+
dispatchVectorGhost(pon, toolId, {
|
|
1894
|
+
t: "rect",
|
|
1895
|
+
rect,
|
|
1896
|
+
ellipse: false
|
|
1897
|
+
});
|
|
1898
|
+
};
|
|
1899
|
+
const clearGhost = () => {
|
|
1900
|
+
if (ctx.getState().toolGhost) ctx.dispatch({
|
|
1901
|
+
type: "SET_TOOL_GHOST",
|
|
1902
|
+
ghost: null
|
|
1903
|
+
});
|
|
1904
|
+
};
|
|
1905
|
+
/** Place a stamp of `desired` PDF-point size centred on a content point — the
|
|
1906
|
+
* one engine-write both the armed and click-to-place paths funnel through.
|
|
1907
|
+
* The size is fit to the page and clamped fully onto it (v2 rubber-stamp
|
|
1908
|
+
* rule: never larger than the page, aspect preserved), and never spills off
|
|
1909
|
+
* the edge. `rotCW` (the tool's upright counter-rotation, CW content degrees)
|
|
1910
|
+
* emits the repository's box rotation fields — the engine bakes the tilted
|
|
1911
|
+
* /AP exactly as an interactively rotated stamp round-trips, and the fit uses
|
|
1912
|
+
* the ROTATED footprint. Returns false when the page/document isn't ready. */
|
|
1913
|
+
const createStampAt = (pon, point, source, desired, rotCW = 0) => {
|
|
1914
|
+
const doc = ctx.doc;
|
|
1915
|
+
const crop = cropOf(pon);
|
|
1916
|
+
if (!doc || !crop) return false;
|
|
1917
|
+
const box = fitStampBox(point, desired, {
|
|
1918
|
+
width: crop.right - crop.left,
|
|
1919
|
+
height: crop.top - crop.bottom
|
|
1920
|
+
}, rotCW);
|
|
1921
|
+
doc.page(pon).annotations.create({
|
|
1922
|
+
subtype: "stamp",
|
|
1923
|
+
...boxGeomFields(box, rotCW, crop),
|
|
1924
|
+
source,
|
|
1925
|
+
fit: "contain"
|
|
1926
|
+
}).then((res) => {
|
|
1927
|
+
syncDTO(res.created, "baked");
|
|
1928
|
+
apply({
|
|
1929
|
+
t: "select",
|
|
1930
|
+
ids: [refKey(res.created.ref)]
|
|
1931
|
+
});
|
|
1932
|
+
}, (err) => console.error("[annotation] stamp placement failed:", err));
|
|
1933
|
+
return true;
|
|
1934
|
+
};
|
|
1935
|
+
/** The active tool's upright counter-rotation for a click at `displayRotation`
|
|
1936
|
+
* (0 when the tool doesn't ask for upright, or the display isn't rotated). */
|
|
1937
|
+
const uprightRotFor = (displayRotation) => {
|
|
1938
|
+
if (!displayRotation) return 0;
|
|
1939
|
+
const ix = ctx.tryGet(InteractionToken);
|
|
1940
|
+
return (ix ? registry.get(ix.activeToolId()) : void 0)?.upright ? uprightRotation(displayRotation) : 0;
|
|
1941
|
+
};
|
|
1942
|
+
const placeArmedStamp = (pon, point, displayRotation) => {
|
|
1943
|
+
const armed = armedStamp;
|
|
1944
|
+
if (!armed) return false;
|
|
1945
|
+
return createStampAt(pon, point, armed.source, {
|
|
1946
|
+
width: armed.width,
|
|
1947
|
+
height: armed.height
|
|
1948
|
+
}, uprightRotFor(displayRotation));
|
|
1949
|
+
};
|
|
1950
|
+
/** Sniff a stamp source (rejecting non-image bytes) and place it at its
|
|
1951
|
+
* intrinsic size (fit to the page in {@link createStampAt}); `targetWidth`
|
|
1952
|
+
* overrides the width, aspect preserved. */
|
|
1953
|
+
const placeStampSource = async (pon, point, source, rotCW, targetWidth) => {
|
|
1954
|
+
const meta = sniffBinaryMetadata((await resolveBinarySource(source)).bytes);
|
|
1955
|
+
if (!meta) {
|
|
1956
|
+
console.error("[annotation] stamp source must be PNG, JPEG, or single-page PDF bytes");
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
createStampAt(pon, point, source, desiredStampSize(meta, targetWidth), rotCW);
|
|
1960
|
+
};
|
|
1961
|
+
/**
|
|
1962
|
+
* Click-to-place: resolve the ACTIVE tool's source spec. Fixed `bytes` place
|
|
1963
|
+
* immediately; a `'prompt'` source asks the installed provider, then places on
|
|
1964
|
+
* resolve — dropping the placement if it was cancelled, or if the tool or
|
|
1965
|
+
* document changed while the picker was open (the intent expired).
|
|
1966
|
+
*/
|
|
1967
|
+
const requestStampAt = (pon, point, displayRotation) => {
|
|
1968
|
+
const ix = ctx.tryGet(InteractionToken);
|
|
1969
|
+
const tool = ix ? registry.get(ix.activeToolId()) : void 0;
|
|
1970
|
+
const spec = tool?.source;
|
|
1971
|
+
if (!spec) return false;
|
|
1972
|
+
const rotCW = tool.upright && displayRotation ? uprightRotation(displayRotation) : 0;
|
|
1973
|
+
if (spec.kind === "bytes") {
|
|
1974
|
+
placeStampSource(pon, point, spec.source, rotCW);
|
|
1975
|
+
return true;
|
|
1976
|
+
}
|
|
1977
|
+
return promptFileAt(tool, pon, point, (picked) => void placeStampSource(pon, point, picked.data, rotCW));
|
|
1978
|
+
};
|
|
1979
|
+
/**
|
|
1980
|
+
* Run the installed file-picker port for a click-then-pick tool, with the
|
|
1981
|
+
* shared expiry rule: the placement is dropped if the picker cancels, or if
|
|
1982
|
+
* the document or the active tool changed while it was open (the intent
|
|
1983
|
+
* expired with the click). No provider installed → decline the click (let a
|
|
1984
|
+
* lower-priority handler act) rather than swallow it.
|
|
1985
|
+
*/
|
|
1986
|
+
const promptFileAt = (tool, pon, point, place) => {
|
|
1987
|
+
const provider = filePickerProvider;
|
|
1988
|
+
if (!provider) return false;
|
|
1989
|
+
const spec = tool.source;
|
|
1990
|
+
const req = {
|
|
1991
|
+
toolId: tool.id,
|
|
1992
|
+
subtype: tool.subtype,
|
|
1993
|
+
accept: spec?.kind === "prompt" ? spec.accept : void 0,
|
|
1994
|
+
pon,
|
|
1995
|
+
point
|
|
1996
|
+
};
|
|
1997
|
+
const docAtClick = ctx.doc;
|
|
1998
|
+
provider(req).then((picked) => {
|
|
1999
|
+
if (!picked) return;
|
|
2000
|
+
if (ctx.doc !== docAtClick) return;
|
|
2001
|
+
if (ctx.tryGet(InteractionToken)?.activeToolId() !== tool.id) return;
|
|
2002
|
+
place(picked);
|
|
2003
|
+
}, (err) => console.error("[annotation] file-picker provider failed:", err));
|
|
2004
|
+
return true;
|
|
2005
|
+
};
|
|
2006
|
+
/** Place a fixed-size ICON annotation (note / file attachment) centred on a
|
|
2007
|
+
* content point — the icon-kind sibling of {@link createStampAt}. The
|
|
2008
|
+
* engine bakes the 20×20 /AP from /C + /Name; a successful create selects
|
|
2009
|
+
* the new annotation (the anchor for its menu / future comment popup). */
|
|
2010
|
+
const createIconAt = (tool, pon, point, rotCW, file) => {
|
|
2011
|
+
const doc = ctx.doc;
|
|
2012
|
+
const crop = cropOf(pon);
|
|
2013
|
+
if (!doc || !crop || !isIconPlaceKind(tool.subtype)) return false;
|
|
2014
|
+
const box = fitStampBox(point, ICON_PLACE_SIZE, {
|
|
2015
|
+
width: crop.right - crop.left,
|
|
2016
|
+
height: crop.top - crop.bottom
|
|
2017
|
+
}, rotCW);
|
|
2018
|
+
const draft = iconPlacementDraft(tool.subtype, boxGeomFields(box, rotCW, crop), defaultsFor(model(), tool.preset), tool.flags, file);
|
|
2019
|
+
doc.page(pon).annotations.create(draft).then((res) => {
|
|
2020
|
+
syncDTO(res.created, "baked");
|
|
2021
|
+
apply({
|
|
2022
|
+
t: "select",
|
|
2023
|
+
ids: [refKey(res.created.ref)]
|
|
2024
|
+
});
|
|
2025
|
+
}, (err) => console.error("[annotation] icon placement failed:", err));
|
|
2026
|
+
return true;
|
|
2027
|
+
};
|
|
2028
|
+
/**
|
|
2029
|
+
* The ONE click-to-place entry the place handler forwards every down to —
|
|
2030
|
+
* armed payload first, then the active tool's kind decides:
|
|
2031
|
+
* - stamp → the source spec (fixed bytes, or the file-picker port)
|
|
2032
|
+
* - note (text) → place immediately (no payload)
|
|
2033
|
+
* - attachment → spot first, file second: the file-picker port opens
|
|
2034
|
+
* inside the click gesture, and placement lands when it
|
|
2035
|
+
* resolves (dropped on cancel, or if the tool/document
|
|
2036
|
+
* changed while the picker was open).
|
|
2037
|
+
* Returns whether the click was consumed.
|
|
2038
|
+
*/
|
|
2039
|
+
const placeAt = (pon, point, displayRotation) => {
|
|
2040
|
+
if (placeArmedStamp(pon, point, displayRotation)) return true;
|
|
2041
|
+
const ix = ctx.tryGet(InteractionToken);
|
|
2042
|
+
const tool = ix ? registry.get(ix.activeToolId()) : void 0;
|
|
2043
|
+
if (!tool) return false;
|
|
2044
|
+
if (tool.subtype === "stamp") return requestStampAt(pon, point, displayRotation);
|
|
2045
|
+
if (!isIconPlaceKind(tool.subtype)) return false;
|
|
2046
|
+
const rotCW = tool.upright && displayRotation ? uprightRotation(displayRotation) : 0;
|
|
2047
|
+
if (tool.subtype === "text") return createIconAt(tool, pon, point, rotCW, null);
|
|
2048
|
+
return promptFileAt(tool, pon, point, (picked) => createIconAt(tool, pon, point, rotCW, picked));
|
|
2049
|
+
};
|
|
2050
|
+
/** The page a ref lives on: from the loaded model first (covers obj/nm refs
|
|
2051
|
+
* that don't carry a pon), else the ref itself (index refs do). */
|
|
2052
|
+
const ponForRef = (ref) => model().byId[refKey(ref)]?.pon ?? (ref.kind === "index" ? ref.pageObjectNumber : null);
|
|
2053
|
+
/**
|
|
2054
|
+
* Re-sync one annotation into the model from the authoritative engine DTO,
|
|
2055
|
+
* with the render `source` the caller decides: `'vector'` when WE authored or
|
|
2056
|
+
* changed the appearance (create / restyle / resize), `'baked'` when the AP is
|
|
2057
|
+
* still authoritative (a move, which preserves it, or a remote edit).
|
|
2058
|
+
* `bumpAp` marks the upsert as confirming an engine /AP re-bake with new
|
|
2059
|
+
* content, so the annotation's `apVersion` — and with it the page's
|
|
2060
|
+
* appearance epoch — advances and the shell fetches the fresh raster.
|
|
2061
|
+
*/
|
|
2062
|
+
const syncDTO = (dto, source, bumpAp = false) => {
|
|
2063
|
+
const crop = cropOf(dto.pageObjectNumber);
|
|
2064
|
+
if (crop) apply({
|
|
2065
|
+
t: "upsert",
|
|
2066
|
+
annots: [fromDTO(dto, crop, source)],
|
|
2067
|
+
bumpAp
|
|
2068
|
+
});
|
|
2069
|
+
};
|
|
2070
|
+
/** The one engine-update path, shared by `update` and `updateSelection`. A
|
|
2071
|
+
* programmatic patch changes the appearance → render live (vector). */
|
|
2072
|
+
const updateOne = async (ref, patch) => {
|
|
2073
|
+
const doc = ctx.doc;
|
|
2074
|
+
if (!doc) throw new Error("[annotation] no document bound");
|
|
2075
|
+
const pon = ponForRef(ref);
|
|
2076
|
+
if (pon == null) throw new Error("[annotation] cannot resolve page for ref");
|
|
2077
|
+
const res = await doc.page(pon).annotations.update(ref, patch);
|
|
2078
|
+
syncDTO(res.updated, "vector");
|
|
2079
|
+
};
|
|
2080
|
+
function apply(msg) {
|
|
2081
|
+
const [next, effects] = update(model(), msg);
|
|
2082
|
+
ctx.dispatch({
|
|
2083
|
+
type: "SET_MODEL",
|
|
2084
|
+
model: next
|
|
2085
|
+
});
|
|
2086
|
+
for (const fx of effects) perform(fx, next);
|
|
2087
|
+
}
|
|
2088
|
+
/**
|
|
2089
|
+
* Per-parent serialization of attached-link reconciles: rapid edits chain
|
|
2090
|
+
* instead of interleaving (two overlapping runs could double-create
|
|
2091
|
+
* children). Each run reads the CURRENT model at execution time, so a
|
|
2092
|
+
* chained run converges on the latest desired state.
|
|
2093
|
+
*/
|
|
2094
|
+
const linkSyncChains = /* @__PURE__ */ new Map();
|
|
2095
|
+
const scheduleLinkSync = (id) => {
|
|
2096
|
+
const next = (linkSyncChains.get(id) ?? Promise.resolve()).then(() => reconcileLinkChildren(id));
|
|
2097
|
+
linkSyncChains.set(id, next);
|
|
2098
|
+
next.finally(() => {
|
|
2099
|
+
if (linkSyncChains.get(id) === next) linkSyncChains.delete(id);
|
|
2100
|
+
});
|
|
2101
|
+
};
|
|
2102
|
+
/**
|
|
2103
|
+
* THE one place attached link children are created, retargeted, re-rected,
|
|
2104
|
+
* or deleted (the delete-with-parent expansion in the core is the one other
|
|
2105
|
+
* consumer of `linkRefs`). Declarative: desired state is derived fresh from
|
|
2106
|
+
* the parent's `link` value + committed geometry (`linkChildRects`), then
|
|
2107
|
+
* diffed against the join keys the last fold reported. Idempotent — foreign
|
|
2108
|
+
* inconsistencies heal on the next local edit.
|
|
2109
|
+
*/
|
|
2110
|
+
const reconcileLinkChildren = async (id) => {
|
|
2111
|
+
const doc = ctx.doc;
|
|
2112
|
+
const a = model().byId[id];
|
|
2113
|
+
if (!doc || !a || !a.ref || a.subtype === "link") return;
|
|
2114
|
+
const crop = cropOf(a.pon);
|
|
2115
|
+
if (!crop) return;
|
|
2116
|
+
const page = doc.page(a.pon);
|
|
2117
|
+
const target = writableTarget(a.link);
|
|
2118
|
+
const rects = a.link == null ? [] : linkChildRects(a).map((r) => contentToPdfRect(r, crop));
|
|
2119
|
+
const current = a.linkRefs ?? [];
|
|
2120
|
+
const nextRefs = [];
|
|
2121
|
+
try {
|
|
2122
|
+
const paired = Math.min(current.length, rects.length);
|
|
2123
|
+
for (let i = 0; i < paired; i++) {
|
|
2124
|
+
const res = await page.annotations.update(current[i], {
|
|
2125
|
+
subtype: "link",
|
|
2126
|
+
rect: rects[i],
|
|
2127
|
+
...target ? { target } : {}
|
|
2128
|
+
});
|
|
2129
|
+
nextRefs.push(res.updated.ref);
|
|
2130
|
+
}
|
|
2131
|
+
for (let i = current.length; i < rects.length; i++) {
|
|
2132
|
+
const res = await page.annotations.create({
|
|
2133
|
+
subtype: "link",
|
|
2134
|
+
rect: rects[i],
|
|
2135
|
+
target,
|
|
2136
|
+
inReplyTo: a.ref,
|
|
2137
|
+
replyType: "group"
|
|
2138
|
+
});
|
|
2139
|
+
nextRefs.push(res.created.ref);
|
|
2140
|
+
}
|
|
2141
|
+
for (let i = rects.length; i < current.length; i++) await page.annotations.delete(current[i]);
|
|
2142
|
+
} catch (err) {
|
|
2143
|
+
console.error("[annotation] attached-link sync failed:", err);
|
|
2144
|
+
}
|
|
2145
|
+
const cur = model().byId[id];
|
|
2146
|
+
if (cur) apply({
|
|
2147
|
+
t: "upsert",
|
|
2148
|
+
annots: [{
|
|
2149
|
+
...cur,
|
|
2150
|
+
linkRefs: nextRefs
|
|
2151
|
+
}]
|
|
2152
|
+
});
|
|
2153
|
+
};
|
|
2154
|
+
/** A relationship-only engine patch (sets/clears `/IRT` + `/RT`) — geometry and
|
|
2155
|
+
* style are left untouched, so grouping never re-bakes an appearance. */
|
|
2156
|
+
const relationshipPatch = (subtype, rel) => ({
|
|
2157
|
+
subtype,
|
|
2158
|
+
...rel
|
|
2159
|
+
});
|
|
2160
|
+
/** Write a relationship change to one committed annotation and re-sync it from
|
|
2161
|
+
* the authoritative DTO (preserving its render source — relationships don't
|
|
2162
|
+
* change the appearance). */
|
|
2163
|
+
const writeRelationship = async (a, rel) => {
|
|
2164
|
+
const doc = ctx.doc;
|
|
2165
|
+
if (!doc || !a.ref || !a.data) return;
|
|
2166
|
+
const res = await doc.page(a.pon).annotations.update(a.ref, relationshipPatch(a.data.subtype, rel));
|
|
2167
|
+
syncDTO(res.updated, a.source);
|
|
2168
|
+
};
|
|
2169
|
+
/** Committed, data-backed annotations in the current selection. */
|
|
2170
|
+
const selectedCommitted = () => {
|
|
2171
|
+
const m = model();
|
|
2172
|
+
return m.selected.map((id) => m.byId[id]).filter((a) => !!a && !!a.ref && !!a.data);
|
|
2173
|
+
};
|
|
2174
|
+
function perform(fx, m) {
|
|
2175
|
+
const doc = ctx.doc;
|
|
2176
|
+
if (!doc) return;
|
|
2177
|
+
if (fx.fx === "createGroup") {
|
|
2178
|
+
const ids = [fx.primary, ...fx.members];
|
|
2179
|
+
const annots = ids.map((id) => m.byId[id]);
|
|
2180
|
+
const primary = annots[0];
|
|
2181
|
+
if (!primary || annots.some((a) => !a || a.pon !== primary.pon)) {
|
|
2182
|
+
apply({
|
|
2183
|
+
t: "remove",
|
|
2184
|
+
ids
|
|
2185
|
+
});
|
|
2186
|
+
return;
|
|
2187
|
+
}
|
|
2188
|
+
const crop = cropOf(primary.pon);
|
|
2189
|
+
const drafts = crop ? annots.map((a) => a ? toCreateDraft(a, crop) : null) : annots.map(() => null);
|
|
2190
|
+
if (drafts.some((d) => !d)) {
|
|
2191
|
+
apply({
|
|
2192
|
+
t: "remove",
|
|
2193
|
+
ids
|
|
2194
|
+
});
|
|
2195
|
+
return;
|
|
2196
|
+
}
|
|
2197
|
+
(async () => {
|
|
2198
|
+
const committed = [];
|
|
2199
|
+
try {
|
|
2200
|
+
const page = doc.page(primary.pon);
|
|
2201
|
+
const primaryResult = await page.annotations.create(drafts[0]);
|
|
2202
|
+
committed.push({
|
|
2203
|
+
tempId: fx.primary,
|
|
2204
|
+
ref: primaryResult.created.ref
|
|
2205
|
+
});
|
|
2206
|
+
apply({
|
|
2207
|
+
t: "created",
|
|
2208
|
+
tempId: fx.primary,
|
|
2209
|
+
id: refKey(primaryResult.created.ref),
|
|
2210
|
+
ref: primaryResult.created.ref
|
|
2211
|
+
});
|
|
2212
|
+
syncDTO(primaryResult.created, "vector");
|
|
2213
|
+
for (let i = 0; i < fx.members.length; i++) {
|
|
2214
|
+
const tempId = fx.members[i];
|
|
2215
|
+
const draft = {
|
|
2216
|
+
...drafts[i + 1],
|
|
2217
|
+
inReplyTo: primaryResult.created.ref,
|
|
2218
|
+
replyType: "group"
|
|
2219
|
+
};
|
|
2220
|
+
const result = await page.annotations.create(draft);
|
|
2221
|
+
committed.push({
|
|
2222
|
+
tempId,
|
|
2223
|
+
ref: result.created.ref
|
|
2224
|
+
});
|
|
2225
|
+
apply({
|
|
2226
|
+
t: "created",
|
|
2227
|
+
tempId,
|
|
2228
|
+
id: refKey(result.created.ref),
|
|
2229
|
+
ref: result.created.ref
|
|
2230
|
+
});
|
|
2231
|
+
syncDTO(result.created, "vector");
|
|
2232
|
+
}
|
|
2233
|
+
} catch (error) {
|
|
2234
|
+
const removeIds = ids.filter((id) => !committed.some((c) => c.tempId === id));
|
|
2235
|
+
for (const part of [...committed].reverse()) try {
|
|
2236
|
+
await doc.page(primary.pon).annotations.delete(part.ref);
|
|
2237
|
+
removeIds.push(refKey(part.ref));
|
|
2238
|
+
} catch {}
|
|
2239
|
+
if (removeIds.length) apply({
|
|
2240
|
+
t: "remove",
|
|
2241
|
+
ids: removeIds
|
|
2242
|
+
});
|
|
2243
|
+
console.error("[annotation] grouped annotation creation failed:", error);
|
|
2244
|
+
}
|
|
2245
|
+
})();
|
|
2246
|
+
} else if (fx.fx === "create") {
|
|
2247
|
+
const a = m.byId[fx.id];
|
|
2248
|
+
const crop = a && cropOf(a.pon);
|
|
2249
|
+
const draft = a && crop ? toCreateDraft(a, crop) : null;
|
|
2250
|
+
if (!a || !draft) return;
|
|
2251
|
+
doc.page(a.pon).annotations.create(draft).then((res) => {
|
|
2252
|
+
apply({
|
|
2253
|
+
t: "created",
|
|
2254
|
+
tempId: fx.id,
|
|
2255
|
+
id: refKey(res.created.ref),
|
|
2256
|
+
ref: res.created.ref
|
|
2257
|
+
});
|
|
2258
|
+
syncDTO(res.created, "vector");
|
|
2259
|
+
}, () => apply({
|
|
2260
|
+
t: "createFailed",
|
|
2261
|
+
tempId: fx.id
|
|
2262
|
+
}));
|
|
2263
|
+
} else if (fx.fx === "flags") {
|
|
2264
|
+
const a = m.byId[fx.id];
|
|
2265
|
+
if (!a || !a.ref || !a.data) return;
|
|
2266
|
+
doc.page(a.pon).annotations.update(a.ref, {
|
|
2267
|
+
subtype: a.data.subtype,
|
|
2268
|
+
flags: a.flags
|
|
2269
|
+
}).then((res) => syncDTO(res.updated, a.source), (err) => console.error("[annotation] flags write failed:", err));
|
|
2270
|
+
} else if (fx.fx === "patch") {
|
|
2271
|
+
const a = m.byId[fx.id];
|
|
2272
|
+
const crop = a && cropOf(a.pon);
|
|
2273
|
+
const patch = a && a.ref && crop ? toScopedPatch(a, fx.scope, crop) : null;
|
|
2274
|
+
if (!a || !a.ref || !patch) return;
|
|
2275
|
+
doc.page(a.pon).annotations.update(a.ref, patch).then((res) => {
|
|
2276
|
+
syncDTO(res.updated, a.source, res.appearance.changed);
|
|
2277
|
+
if (a.linkRefs?.length) scheduleLinkSync(fx.id);
|
|
2278
|
+
}, () => {});
|
|
2279
|
+
} else if (fx.fx === "syncLink") scheduleLinkSync(fx.id);
|
|
2280
|
+
else doc.page(fx.ref.pageObjectNumber).annotations.delete(fx.ref).then(() => {}, () => {});
|
|
2281
|
+
}
|
|
2282
|
+
return {
|
|
2283
|
+
create: async (pon, draft) => {
|
|
2284
|
+
const doc = ctx.doc;
|
|
2285
|
+
if (!doc) throw new Error("[annotation] no document bound");
|
|
2286
|
+
const withFlags = draft.flags ? draft : {
|
|
2287
|
+
...draft,
|
|
2288
|
+
flags: { print: true }
|
|
2289
|
+
};
|
|
2290
|
+
const res = await doc.page(pon).annotations.create(withFlags);
|
|
2291
|
+
syncDTO(res.created, res.created.subtype === "stamp" ? "baked" : "vector");
|
|
2292
|
+
return res.created.ref;
|
|
2293
|
+
},
|
|
2294
|
+
armStamp,
|
|
2295
|
+
disarmStamp,
|
|
2296
|
+
placeArmedStamp,
|
|
2297
|
+
requestStampAt,
|
|
2298
|
+
placeAt,
|
|
2299
|
+
hasArmedStamp: () => armedStamp != null,
|
|
2300
|
+
ghostHoverAt,
|
|
2301
|
+
clearGhost,
|
|
2302
|
+
setPlacementPreview,
|
|
2303
|
+
clearPlacementPreview: clearGhost,
|
|
2304
|
+
toolGhost: (pon) => {
|
|
2305
|
+
const g = ctx.getState().toolGhost;
|
|
2306
|
+
return g && g.pon === pon ? g : null;
|
|
2307
|
+
},
|
|
2308
|
+
armedStampPreview: () => armedStamp?.preview ?? null,
|
|
2309
|
+
stampArmEpoch: () => ctx.getState().stampArmEpoch,
|
|
2310
|
+
setFilePickerProvider: (provider) => {
|
|
2311
|
+
filePickerProvider = provider;
|
|
2312
|
+
},
|
|
2313
|
+
downloadAttachment: async (ref) => {
|
|
2314
|
+
const doc = ctx.doc;
|
|
2315
|
+
if (!doc) throw new Error("[annotation] no document bound");
|
|
2316
|
+
const pon = ponForRef(ref);
|
|
2317
|
+
if (pon == null) throw new Error("[annotation] cannot resolve page for ref");
|
|
2318
|
+
const annotations = doc.page(pon).annotations;
|
|
2319
|
+
if (!annotations.downloadFile) throw new Error("[annotation] this engine does not support attachment download");
|
|
2320
|
+
return annotations.downloadFile(ref);
|
|
2321
|
+
},
|
|
2322
|
+
tools: () => [...registry.values()],
|
|
2323
|
+
tool: (id) => registry.get(id) ?? null,
|
|
2324
|
+
toolSubtype: (id) => registry.get(id)?.subtype ?? id,
|
|
2325
|
+
registerTool: (def) => {
|
|
2326
|
+
const resolved = buildToolRegistry([...configTools, def]).get(def.id);
|
|
2327
|
+
if (!resolved) throw new Error(`[annotation] could not resolve tool '${def.id}'`);
|
|
2328
|
+
registry.set(resolved.id, resolved);
|
|
2329
|
+
const un = ctx.tryGet(InteractionToken)?.registerTool({
|
|
2330
|
+
id: resolved.id,
|
|
2331
|
+
cursor: resolved.cursor,
|
|
2332
|
+
enables: resolved.enables
|
|
2333
|
+
});
|
|
2334
|
+
if (resolved.defaults) apply({
|
|
2335
|
+
t: "setDefaults",
|
|
2336
|
+
subtype: resolved.preset,
|
|
2337
|
+
patch: resolved.defaults
|
|
2338
|
+
});
|
|
2339
|
+
return () => {
|
|
2340
|
+
registry.delete(resolved.id);
|
|
2341
|
+
un?.();
|
|
2342
|
+
};
|
|
2343
|
+
},
|
|
2344
|
+
update: (ref, patch) => updateOne(ref, patch),
|
|
2345
|
+
updateSelection: (patch) => apply({
|
|
2346
|
+
t: "setProps",
|
|
2347
|
+
patch
|
|
2348
|
+
}),
|
|
2349
|
+
updateSelectionFlags: (patch) => apply({
|
|
2350
|
+
t: "setFlags",
|
|
2351
|
+
patch
|
|
2352
|
+
}),
|
|
2353
|
+
getSelectionFlags: () => memoSelectionFlags(),
|
|
2354
|
+
delete: async (ref) => {
|
|
2355
|
+
const doc = ctx.doc;
|
|
2356
|
+
if (!doc) throw new Error("[annotation] no document bound");
|
|
2357
|
+
const pon = ponForRef(ref);
|
|
2358
|
+
if (pon == null) throw new Error("[annotation] cannot resolve page for ref");
|
|
2359
|
+
await doc.page(pon).annotations.delete(ref);
|
|
2360
|
+
apply({
|
|
2361
|
+
t: "remove",
|
|
2362
|
+
ids: [refKey(ref)]
|
|
2363
|
+
});
|
|
2364
|
+
},
|
|
2365
|
+
canCreate: () => ctx.doc?.security.allows(ANNOTATE_MODIFY) ?? false,
|
|
2366
|
+
canEdit: () => ctx.doc?.security.allows(ANNOTATE_MODIFY) ?? false,
|
|
2367
|
+
canDelete: () => ctx.doc?.security.allows(ANNOTATE_MODIFY) ?? false,
|
|
2368
|
+
getSelection: () => {
|
|
2369
|
+
const m = model();
|
|
2370
|
+
return m.selected.map((id) => m.byId[id]?.ref).filter((r) => r != null);
|
|
2371
|
+
},
|
|
2372
|
+
get: (ref) => model().byId[refKey(ref)]?.data ?? null,
|
|
2373
|
+
list: (pon) => {
|
|
2374
|
+
const m = model();
|
|
2375
|
+
return m.order.map((id) => m.byId[id]).filter((a) => a?.pon === pon).map((a) => a?.data).filter((d) => d != null);
|
|
2376
|
+
},
|
|
2377
|
+
getSelected: () => {
|
|
2378
|
+
const m = model();
|
|
2379
|
+
return m.selected.map((id) => m.byId[id]?.data).filter((d) => d != null);
|
|
2380
|
+
},
|
|
2381
|
+
getSelectionProps: () => memoSelectionProps(),
|
|
2382
|
+
propsForTool: (toolId) => propsFor(registry.get(toolId)?.propsKind ?? toolId),
|
|
2383
|
+
pageItems: (pon, view) => memoItems(pon, view),
|
|
2384
|
+
chrome: (pon, scale, rotation, zoom) => memoChrome(pon, scale, rotation, zoom),
|
|
2385
|
+
selectionAnchor: (scale, rotation, zoom) => memoAnchor(scale, rotation, zoom),
|
|
2386
|
+
creationDraftAnchor: () => memoDraftAnchor(),
|
|
2387
|
+
selection: () => model().selected,
|
|
2388
|
+
hitKind: (pon, point, scale, rotation, zoom) => hitTest(model(), pon, point, chromeGeomAt(scale), model().hitMargin, pageBoxOf(pon), inertIdsAt(pon), viewEnv(zoom, rotation)).t,
|
|
2389
|
+
cursorAt: (pon, point, scale, rotation, zoom) => cursorAt(model(), pon, point, chromeGeomAt(scale), model().hitMargin, pageBoxOf(pon), inertIdsAt(pon), viewEnv(zoom, rotation)),
|
|
2390
|
+
hoverAt: (at) => {
|
|
2391
|
+
const m = model();
|
|
2392
|
+
let id = null;
|
|
2393
|
+
if (at) {
|
|
2394
|
+
const h = hitTest(m, at.pon, at.point, chromeGeomAt(at.scale), m.hitMargin, pageBoxOf(at.pon), inertIdsAt(at.pon), viewEnv(at.zoom, at.rotation));
|
|
2395
|
+
if (h.t === "annot") id = h.id;
|
|
2396
|
+
}
|
|
2397
|
+
if (m.hovered !== id) apply({
|
|
2398
|
+
t: "hover",
|
|
2399
|
+
id
|
|
2400
|
+
});
|
|
2401
|
+
},
|
|
2402
|
+
behaviorFor: (a) => behaviors.find((b) => b.matches(a) && b.engaged()) ?? null,
|
|
2403
|
+
linkItemsOn: (pon) => memoLinkItems(pon),
|
|
2404
|
+
appearanceEpoch: (pon) => {
|
|
2405
|
+
const m = model();
|
|
2406
|
+
const parts = [];
|
|
2407
|
+
for (const id of m.order) {
|
|
2408
|
+
const a = m.byId[id];
|
|
2409
|
+
if (!a || a.pon !== pon || a.source !== "baked" || !a.ref) continue;
|
|
2410
|
+
parts.push(`${id}@${a.apVersion ?? 0}`);
|
|
2411
|
+
}
|
|
2412
|
+
return parts.sort().join("|");
|
|
2413
|
+
},
|
|
2414
|
+
bakeScale: (renderScale) => snapAppearanceScale(ctx.document()?.renderPolicy ?? CONTINUOUS_RENDER_POLICY, renderScale),
|
|
2415
|
+
appearances: (pon, scale, signal) => {
|
|
2416
|
+
const doc = ctx.doc;
|
|
2417
|
+
if (!doc) return Promise.resolve([]);
|
|
2418
|
+
const task = doc.page(pon).annotations.renderAppearanceImages({ scale });
|
|
2419
|
+
if (signal) if (signal.aborted) task.abort(signal.reason);
|
|
2420
|
+
else signal.addEventListener("abort", () => task.abort(signal.reason), { once: true });
|
|
2421
|
+
return task.then((r) => r.appearances, () => []);
|
|
2422
|
+
},
|
|
2423
|
+
toContentBox: (pon, rect) => {
|
|
2424
|
+
const crop = cropOf(pon);
|
|
2425
|
+
return crop ? pdfToContentRect(rect, crop) : null;
|
|
2426
|
+
},
|
|
2427
|
+
editPointer: (phase, pon, point, shift, scale, rotation, zoom) => apply({
|
|
2428
|
+
t: "editPointer",
|
|
2429
|
+
phase,
|
|
2430
|
+
in: {
|
|
2431
|
+
pon,
|
|
2432
|
+
point,
|
|
2433
|
+
shift,
|
|
2434
|
+
pageBox: pageBoxOf(pon),
|
|
2435
|
+
chrome: chromeGeomAt(scale),
|
|
2436
|
+
inert: inertIdsAt(pon),
|
|
2437
|
+
...zoom != null ? { zoom } : {},
|
|
2438
|
+
...rotation != null ? { displayRotation: rotation } : {}
|
|
2439
|
+
}
|
|
2440
|
+
}),
|
|
2441
|
+
marqueePointer: (phase, pon, point, shift, scale, rotation, zoom) => apply({
|
|
2442
|
+
t: "marqueePointer",
|
|
2443
|
+
phase,
|
|
2444
|
+
in: {
|
|
2445
|
+
pon,
|
|
2446
|
+
point,
|
|
2447
|
+
shift,
|
|
2448
|
+
pageBox: pageBoxOf(pon),
|
|
2449
|
+
inert: inertIdsAt(pon),
|
|
2450
|
+
...zoom != null ? { zoom } : {},
|
|
2451
|
+
...rotation != null ? { displayRotation: rotation } : {}
|
|
2452
|
+
}
|
|
2453
|
+
}),
|
|
2454
|
+
createPointer: (tool, phase, pon, point, finish = false, displayRotation) => {
|
|
2455
|
+
const t = registry.get(tool);
|
|
2456
|
+
apply({
|
|
2457
|
+
t: "createPointer",
|
|
2458
|
+
phase,
|
|
2459
|
+
subtype: t?.subtype ?? tool,
|
|
2460
|
+
preset: t?.preset ?? tool,
|
|
2461
|
+
intent: t?.intent,
|
|
2462
|
+
clickCreate: t?.clickCreate,
|
|
2463
|
+
flags: t?.flags,
|
|
2464
|
+
deferInkCommit: (t?.ink?.groupStrokesMs ?? 0) > 0,
|
|
2465
|
+
straightenInk: t?.ink?.straighten,
|
|
2466
|
+
in: {
|
|
2467
|
+
pon,
|
|
2468
|
+
point,
|
|
2469
|
+
shift: false,
|
|
2470
|
+
finish,
|
|
2471
|
+
pageBox: pageBoxOf(pon),
|
|
2472
|
+
displayRotation,
|
|
2473
|
+
upright: t?.upright
|
|
2474
|
+
}
|
|
2475
|
+
});
|
|
2476
|
+
},
|
|
2477
|
+
finishCreationDraft: () => apply({ t: "finishCreationDraft" }),
|
|
2478
|
+
finishInkDraft: () => apply({ t: "finishInkDraft" }),
|
|
2479
|
+
cancelCreationDraft: () => apply({ t: "cancel" }),
|
|
2480
|
+
createMarkup: (subtype, pon, rects, preset) => apply({
|
|
2481
|
+
t: "createMarkup",
|
|
2482
|
+
subtype,
|
|
2483
|
+
pon,
|
|
2484
|
+
rects,
|
|
2485
|
+
preset,
|
|
2486
|
+
flags: preset ? registry.get(preset)?.flags : void 0
|
|
2487
|
+
}),
|
|
2488
|
+
createCaret: (pon, textEndRect) => apply({
|
|
2489
|
+
t: "createCaret",
|
|
2490
|
+
pon,
|
|
2491
|
+
rect: textEndRect
|
|
2492
|
+
}),
|
|
2493
|
+
createReplaceText: (pon, rects, textEndRect, preset) => apply({
|
|
2494
|
+
t: "createReplaceText",
|
|
2495
|
+
pon,
|
|
2496
|
+
rects,
|
|
2497
|
+
endRect: textEndRect,
|
|
2498
|
+
preset
|
|
2499
|
+
}),
|
|
2500
|
+
previewMarkup: (subtype, rectsByPage, preset) => apply({
|
|
2501
|
+
t: "setMarkupPreview",
|
|
2502
|
+
subtype,
|
|
2503
|
+
rectsByPage,
|
|
2504
|
+
preset
|
|
2505
|
+
}),
|
|
2506
|
+
clearMarkupPreview: () => apply({ t: "clearMarkupPreview" }),
|
|
2507
|
+
setDefaults: (subtype, patch) => apply({
|
|
2508
|
+
t: "setDefaults",
|
|
2509
|
+
subtype,
|
|
2510
|
+
patch
|
|
2511
|
+
}),
|
|
2512
|
+
currentDefaults: (toolId) => defaultsFor(model(), registry.get(toolId)?.preset ?? toolId),
|
|
2513
|
+
setSnap: (patch) => apply({
|
|
2514
|
+
t: "setSnap",
|
|
2515
|
+
patch
|
|
2516
|
+
}),
|
|
2517
|
+
snapSettings: () => model().snap,
|
|
2518
|
+
setChrome: (patch) => ctx.dispatch({
|
|
2519
|
+
type: "SET_CHROME",
|
|
2520
|
+
patch
|
|
2521
|
+
}),
|
|
2522
|
+
chromeSettings: () => chromeSettings(),
|
|
2523
|
+
deleteSelection: () => apply({ t: "delete" }),
|
|
2524
|
+
deselect: () => apply({ t: "deselect" }),
|
|
2525
|
+
select: (ref, options) => {
|
|
2526
|
+
apply({
|
|
2527
|
+
t: "select",
|
|
2528
|
+
ids: [refKey(ref)],
|
|
2529
|
+
add: options?.add
|
|
2530
|
+
});
|
|
2531
|
+
},
|
|
2532
|
+
pruneEngagedSelection: () => {
|
|
2533
|
+
const m = model();
|
|
2534
|
+
const drop = m.selected.filter((id) => {
|
|
2535
|
+
const a = m.byId[id];
|
|
2536
|
+
return a && behaviors.some((b) => b.matches({
|
|
2537
|
+
subtype: a.subtype,
|
|
2538
|
+
ref: a.ref
|
|
2539
|
+
}) && b.engaged());
|
|
2540
|
+
});
|
|
2541
|
+
if (drop.length) apply({
|
|
2542
|
+
t: "deselect",
|
|
2543
|
+
ids: drop
|
|
2544
|
+
});
|
|
2545
|
+
},
|
|
2546
|
+
cancel: () => apply({ t: "cancel" }),
|
|
2547
|
+
rotateSelection90: () => apply({ t: "rotate90" }),
|
|
2548
|
+
resetSelectionRotation: () => apply({ t: "resetRotation" }),
|
|
2549
|
+
/** Group the current selection into one unit: the bottom-most member becomes
|
|
2550
|
+
* the primary, every other member becomes a `/RT /Group` subordinate of it.
|
|
2551
|
+
* No-op unless 2+ committed annotations on a single page are selected. */
|
|
2552
|
+
group: async () => {
|
|
2553
|
+
const m = model();
|
|
2554
|
+
const members = selectedCommitted();
|
|
2555
|
+
if (members.length < 2) return;
|
|
2556
|
+
const pon = members[0].pon;
|
|
2557
|
+
if (members.some((a) => a.pon !== pon)) return;
|
|
2558
|
+
const [primary, ...rest] = [...members].sort((a, b) => m.order.indexOf(a.id) - m.order.indexOf(b.id));
|
|
2559
|
+
if (!primary.ref) return;
|
|
2560
|
+
await Promise.all(rest.map((a) => writeRelationship(a, {
|
|
2561
|
+
inReplyTo: primary.ref,
|
|
2562
|
+
replyType: "group"
|
|
2563
|
+
})));
|
|
2564
|
+
},
|
|
2565
|
+
/** Ungroup: clear `/IRT` (+ `/RT`) on every subordinate in the group(s) the
|
|
2566
|
+
* selection touches, so each member becomes top-level again. */
|
|
2567
|
+
ungroup: async () => {
|
|
2568
|
+
const m = model();
|
|
2569
|
+
const subs = expandGroups(m, m.selected).map((id) => m.byId[id]).filter((a) => !!a && !!a.ref && !!a.data && !!a.group);
|
|
2570
|
+
await Promise.all(subs.map((a) => writeRelationship(a, { inReplyTo: null })));
|
|
2571
|
+
},
|
|
2572
|
+
canGroup: () => {
|
|
2573
|
+
if (!(ctx.doc?.security.allows(ANNOTATE_MODIFY) ?? false)) return false;
|
|
2574
|
+
const m = model();
|
|
2575
|
+
const members = selectedCommitted();
|
|
2576
|
+
if (members.length < 2) return false;
|
|
2577
|
+
if (members.some((a) => a.pon !== members[0].pon)) return false;
|
|
2578
|
+
const keys = new Set(m.selected.map((id) => groupKeyOf(m, id)));
|
|
2579
|
+
if (keys.size === 1 && !keys.has(null)) return false;
|
|
2580
|
+
return true;
|
|
2581
|
+
},
|
|
2582
|
+
canUngroup: () => {
|
|
2583
|
+
if (!(ctx.doc?.security.allows(ANNOTATE_MODIFY) ?? false)) return false;
|
|
2584
|
+
const m = model();
|
|
2585
|
+
return m.selected.some((id) => groupKeyOf(m, id) != null);
|
|
2586
|
+
},
|
|
2587
|
+
ensurePage: (pon) => {
|
|
2588
|
+
if (loaded.has(pon)) return;
|
|
2589
|
+
const doc = ctx.doc;
|
|
2590
|
+
const crop = cropOf(pon);
|
|
2591
|
+
if (!doc || !crop) return;
|
|
2592
|
+
loaded.add(pon);
|
|
2593
|
+
doc.page(pon).annotations.list().then((snap) => apply({
|
|
2594
|
+
t: "loaded",
|
|
2595
|
+
annots: foldAttachedLinks(snap.annotations.map((d) => fromDTO(d, crop)))
|
|
2596
|
+
}), () => {
|
|
2597
|
+
loaded.delete(pon);
|
|
2598
|
+
});
|
|
2599
|
+
},
|
|
2600
|
+
reloadPage: async (pon) => {
|
|
2601
|
+
const doc = ctx.doc;
|
|
2602
|
+
const crop = cropOf(pon);
|
|
2603
|
+
if (!doc || !crop) return;
|
|
2604
|
+
loaded.add(pon);
|
|
2605
|
+
try {
|
|
2606
|
+
const snap = await doc.page(pon).annotations.list();
|
|
2607
|
+
const m = model();
|
|
2608
|
+
const stale = m.order.filter((id) => m.byId[id]?.pon === pon);
|
|
2609
|
+
if (stale.length) apply({
|
|
2610
|
+
t: "remove",
|
|
2611
|
+
ids: stale
|
|
2612
|
+
});
|
|
2613
|
+
apply({
|
|
2614
|
+
t: "loaded",
|
|
2615
|
+
annots: foldAttachedLinks(snap.annotations.map((d) => fromDTO(d, crop)))
|
|
2616
|
+
});
|
|
2617
|
+
} catch {
|
|
2618
|
+
loaded.delete(pon);
|
|
2619
|
+
}
|
|
2620
|
+
},
|
|
2621
|
+
textItems: (pon, view) => memoTexts(pon, view),
|
|
2622
|
+
currentEditing: () => model().editing,
|
|
2623
|
+
beginTextEdit: (ref) => apply({
|
|
2624
|
+
t: "beginTextEdit",
|
|
2625
|
+
id: refKey(ref)
|
|
2626
|
+
}),
|
|
2627
|
+
beginTextEditAt: (pon, point, scale, rotation, zoom) => {
|
|
2628
|
+
const m = model();
|
|
2629
|
+
const h = hitTest(m, pon, point, chromeGeomAt(scale), m.hitMargin, pageBoxOf(pon), void 0, viewEnv(zoom, rotation));
|
|
2630
|
+
const id = h.t === "annot" || h.t === "handle" ? h.id : null;
|
|
2631
|
+
if (id != null && m.byId[id]?.geom.t === "text") apply({
|
|
2632
|
+
t: "beginTextEdit",
|
|
2633
|
+
id
|
|
2634
|
+
});
|
|
2635
|
+
},
|
|
2636
|
+
setContents: (ref, text) => {
|
|
2637
|
+
apply({
|
|
2638
|
+
t: "setText",
|
|
2639
|
+
id: refKey(ref),
|
|
2640
|
+
text
|
|
2641
|
+
});
|
|
2642
|
+
const key = refKey(ref);
|
|
2643
|
+
clearTimeout(textTimers.get(key));
|
|
2644
|
+
textTimers.set(key, setTimeout(() => {
|
|
2645
|
+
textTimers.delete(key);
|
|
2646
|
+
const pon = ponForRef(ref);
|
|
2647
|
+
if (pon != null) ctx.doc?.page(pon).annotations.update(ref, {
|
|
2648
|
+
subtype: "free-text",
|
|
2649
|
+
contents: text
|
|
2650
|
+
}).then(() => {}, () => {});
|
|
2651
|
+
}, TEXT_COMMIT_DEBOUNCE_MS));
|
|
2652
|
+
},
|
|
2653
|
+
endTextEdit: () => {
|
|
2654
|
+
for (const t of textTimers.values()) clearTimeout(t);
|
|
2655
|
+
const id = model().editing;
|
|
2656
|
+
const a = id ? model().byId[id] : null;
|
|
2657
|
+
if (a?.ref) {
|
|
2658
|
+
const pon = ponForRef(a.ref);
|
|
2659
|
+
const text = a.data?.contents ?? "";
|
|
2660
|
+
if (pon != null) ctx.doc?.page(pon).annotations.update(a.ref, {
|
|
2661
|
+
subtype: "free-text",
|
|
2662
|
+
contents: text
|
|
2663
|
+
}).then(() => {}, () => {});
|
|
2664
|
+
}
|
|
2665
|
+
textTimers.clear();
|
|
2666
|
+
apply({ t: "endTextEdit" });
|
|
2667
|
+
},
|
|
2668
|
+
registerBehavior: (b) => {
|
|
2669
|
+
behaviors.push(b);
|
|
2670
|
+
return () => {
|
|
2671
|
+
const i = behaviors.indexOf(b);
|
|
2672
|
+
if (i >= 0) behaviors.splice(i, 1);
|
|
2673
|
+
};
|
|
2674
|
+
}
|
|
2675
|
+
};
|
|
2676
|
+
}
|
|
2677
|
+
//#endregion
|
|
2678
|
+
export { toCreateDraft as a, styleFromDTO as c, fromDTO as i, widgetAppearanceFromProps as l, DEFAULT_TOOLS as n, toPatch as o, buildToolRegistry as r, refKey as s, createAnnotationCapability as t, AnnotationToken as u };
|
|
2679
|
+
|
|
2680
|
+
//# sourceMappingURL=capability-fghl0JmE.js.map
|