@embedpdf/plugin-selection 3.0.0-next.5 → 3.0.0-next.7

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/dist/index.cjs CHANGED
@@ -1,7 +1,8 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_reducer = require("./reducer-Dyf8p5h-.cjs");
2
+ const require_reducer = require("./reducer-B6uL_O7s.cjs");
3
3
  let _embedpdf_core = require("@embedpdf/core");
4
4
  let _embedpdf_plugin_interaction = require("@embedpdf/plugin-interaction");
5
+ let _embedpdf_core_geometry = require("@embedpdf/core-geometry");
5
6
  //#region src/selection.plugin.ts
6
7
  /**
7
8
  * Text selection — document-scoped, requires the interaction hub. In `init` it
@@ -18,16 +19,121 @@ const selectionPlugin = () => (0, _embedpdf_core.definePlugin)({
18
19
  token: require_reducer.SelectionToken,
19
20
  scope: "document",
20
21
  requires: [_embedpdf_plugin_interaction.InteractionToken],
22
+ optional: [_embedpdf_plugin_interaction.FeedbackToken],
21
23
  initialState: require_reducer.initialSelectionState,
22
24
  reduce: require_reducer.selectionReducer,
23
25
  capability: require_reducer.createSelectionCapability,
24
26
  init: (ctx) => {
25
27
  const interaction = ctx.get(_embedpdf_plugin_interaction.InteractionToken);
26
28
  const selection = ctx.get(require_reducer.SelectionToken);
27
- interaction.registerHandler(require_reducer.createTextSelectHandler(selection, interaction));
29
+ const feedback = ctx.tryGet(_embedpdf_plugin_interaction.FeedbackToken);
30
+ interaction.registerHandler(require_reducer.createTextSelectHandler(selection, interaction, feedback ?? void 0));
28
31
  }
29
32
  });
30
33
  //#endregion
34
+ //#region src/handles.ts
35
+ /**
36
+ * Selection handles — the pure policy half (the `wheel.ts` convention).
37
+ *
38
+ * A handle IS the boundary glyph's leading (or trailing) edge: a segment
39
+ * between two corners of the glyph's ORIENTED cell, with the grab head just
40
+ * beyond the ascent at the start / past the baseline at the end. Bar length,
41
+ * angle, and head all derive from that one segment, so rotated text and
42
+ * rotated pages are carried by construction — the AABB never enters.
43
+ *
44
+ * Everything here is DOM-free and framework-free: geometry is pure given a
45
+ * projector, and the drag session speaks the selection's own gesture verbs
46
+ * (`beginAt`/`extendTo`/`end` — the same ones the pointer handler uses; a
47
+ * handle drag IS a selection gesture, re-anchored). The framework adapters
48
+ * keep only subscriptions and markup; the DOM listener mechanics live in
49
+ * `@embedpdf/web`'s `attachSelectionHandle`.
50
+ *
51
+ * The view dependency is STRUCTURAL (satisfied by five lines over
52
+ * `StageCapability`) so this plugin stays stage-free — selection also runs
53
+ * in stage-less hosts (`PageView`) — and the math tests run against a fake.
54
+ */
55
+ const HANDLE_HEAD = 12;
56
+ const HANDLE_BAR = 2;
57
+ const HANDLE_PAD = 14;
58
+ /** Tilts within ~0.05° of upright render untransformed (float-noise guard —
59
+ * and the dominant case stays pixel-identical to an axis-aligned box). */
60
+ const HANDLE_ROT_EPSILON = .05;
61
+ const midpoint = (a, b) => ({
62
+ x: (a.x + b.x) / 2,
63
+ y: (a.y + b.y) / 2
64
+ });
65
+ /**
66
+ * The handle's geometry for one endpoint, in overlay space. Null when the
67
+ * endpoint's page isn't laid out (or the cell is degenerate) — render nothing.
68
+ */
69
+ function selectionHandleGeom(view, ep, role) {
70
+ const leading = role === "start" ? ep.advance > 0 : ep.advance < 0;
71
+ const [upperPage, lowerPage] = (0, _embedpdf_core_geometry.textQuadEdge)(ep.glyphQuad, leading ? "start" : "end");
72
+ const upper = view.toOverlay(ep.pon, upperPage);
73
+ const lower = view.toOverlay(ep.pon, lowerPage);
74
+ if (!upper || !lower) return null;
75
+ const dx = lower.x - upper.x;
76
+ const dy = lower.y - upper.y;
77
+ const length = Math.hypot(dx, dy);
78
+ if (length === 0) return null;
79
+ const rotation = Math.atan2(dy, dx) * 180 / Math.PI - 90;
80
+ const upright = Math.abs(rotation) < HANDLE_ROT_EPSILON;
81
+ const ux = dx / length;
82
+ const uy = dy / length;
83
+ const head = role === "start" ? {
84
+ x: upper.x - ux * 12 / 2,
85
+ y: upper.y - uy * 12 / 2
86
+ } : {
87
+ x: lower.x + ux * 12 / 2,
88
+ y: lower.y + uy * 12 / 2
89
+ };
90
+ return {
91
+ bar: {
92
+ from: upper,
93
+ to: lower
94
+ },
95
+ length,
96
+ rotation,
97
+ upright,
98
+ head
99
+ };
100
+ }
101
+ /**
102
+ * One armed handle drag. Dragging a handle extends from the OPPOSITE
103
+ * endpoint: the first movement re-roots the selection gesture at that
104
+ * endpoint's cell centre, and every position then extends toward the pointer
105
+ * — snapping to glyphs and crossing pages exactly like a pointer drag,
106
+ * firing the same change/commit signals. Over a gap the point projects onto
107
+ * the last page hit (unclamped), so the selection keeps tracking instead of
108
+ * freezing at a page edge. `end()` commits only if the drag actually
109
+ * re-rooted; an untouched press settles nothing.
110
+ */
111
+ function createSelectionHandleDrag(selection, view, opposite, draggedPon) {
112
+ const q = opposite.glyphQuad;
113
+ const anchorPoint = midpoint(midpoint(q.upperStart, q.lowerEnd), midpoint(q.upperEnd, q.lowerStart));
114
+ let begun = false;
115
+ let lastPon = draggedPon;
116
+ return {
117
+ move: (overlay) => {
118
+ if (!begun) {
119
+ if (!selection.beginAt(opposite.pon, anchorPoint)) return;
120
+ begun = true;
121
+ }
122
+ const hit = view.pageAt(overlay);
123
+ if (hit) {
124
+ lastPon = hit.pon;
125
+ selection.extendTo(hit.pon, hit.point);
126
+ } else {
127
+ const p = view.pointOnPage(lastPon, overlay);
128
+ if (p) selection.extendTo(lastPon, p);
129
+ }
130
+ },
131
+ end: () => {
132
+ if (begun) selection.end();
133
+ }
134
+ };
135
+ }
136
+ //#endregion
31
137
  //#region src/index.ts
32
138
  /**
33
139
  * The selection capability token, narrowed to the public
@@ -37,7 +143,12 @@ const selectionPlugin = () => (0, _embedpdf_core.definePlugin)({
37
143
  */
38
144
  const SelectionToken = require_reducer.SelectionToken;
39
145
  //#endregion
146
+ exports.HANDLE_BAR = HANDLE_BAR;
147
+ exports.HANDLE_HEAD = HANDLE_HEAD;
148
+ exports.HANDLE_PAD = HANDLE_PAD;
40
149
  exports.SelectionToken = SelectionToken;
150
+ exports.createSelectionHandleDrag = createSelectionHandleDrag;
151
+ exports.selectionHandleGeom = selectionHandleGeom;
41
152
  exports.selectionPlugin = selectionPlugin;
42
153
 
43
154
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","names":["SelectionToken","InteractionToken","initialSelectionState","selectionReducer","createSelectionCapability","createTextSelectHandler","SelectionHostToken"],"sources":["../src/selection.plugin.ts","../src/index.ts"],"sourcesContent":["import { definePlugin } from '@embedpdf/core';\nimport { InteractionToken } from '@embedpdf/plugin-interaction';\nimport { createSelectionCapability } from './capability';\nimport { createTextSelectHandler } from './handler';\nimport { initialSelectionState, selectionReducer } from './reducer';\nimport { SelectionToken } from './types';\nimport type { SelectionAction, SelectionHostCapability, SelectionState } from './types';\n\n/**\n * Text selection — document-scoped, requires the interaction hub. In `init` it\n * registers ITS pointer handler with the hub; the hub owns the pointer stream\n * and arbitration. Works with `<Stage>` or a standalone `<PageView>` — selection\n * only needs the page coordinate context + the engine's text geometry.\n *\n * Registry/content invalidation is wired inside the capability itself\n * (revision watch + document event subscription), so headless/programmatic\n * use gets the same reconciliation as the full viewer.\n */\nexport const selectionPlugin = () =>\n definePlugin<SelectionState, SelectionAction, SelectionHostCapability>({\n id: 'selection',\n token: SelectionToken,\n scope: 'document',\n requires: [InteractionToken],\n initialState: initialSelectionState,\n reduce: selectionReducer,\n capability: createSelectionCapability,\n init: (ctx) => {\n const interaction = ctx.get(InteractionToken);\n const selection = ctx.get(SelectionToken); // our own capability (built before init)\n interaction.registerHandler(createTextSelectHandler(selection, interaction));\n },\n });\n","/**\n * @embedpdf/plugin-selection — text selection over the engine's text geometry.\n *\n * The PUBLIC surface: the plugin factory, the capability token (narrowed to\n * the public lens), and the range/read vocabulary. Selection ranges live in\n * CHARACTER space (half-open `TextRange` — the same space search hits\n * address), geometry needs `doc.text.select`, text extraction needs\n * `doc.text.copy`, and neither permission implies the other.\n *\n * Framework/host plumbing (gesture intents, geometry warming, the reducer,\n * the coordinate seam) lives behind `@embedpdf/plugin-selection/internal`.\n * Clipboard writes live in `@embedpdf/web` — this package is DOM-free.\n */\nimport type { CapabilityToken } from '@embedpdf/core';\nimport { SelectionToken as SelectionHostToken } from './types';\nimport type { SelectionCapability } from './types';\n\nexport { selectionPlugin } from './selection.plugin';\nexport type {\n SelectionCapability,\n SelectionEndpoint,\n SelectionMenuAnchor,\n SelectionRangeInput,\n SelectionSnapshot,\n TextPosition,\n TextRange,\n} from './types';\nexport type { SelectionSegment } from './geometry';\n\n/**\n * The selection capability token, narrowed to the public\n * {@link SelectionCapability} lens. It is the SAME runtime token the plugin\n * provides — the host-only surface (pointer gestures, geometry warming) is\n * reachable only via `@embedpdf/plugin-selection/internal`.\n */\nexport const SelectionToken =\n SelectionHostToken as unknown as CapabilityToken<SelectionCapability>;\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,MAAa,yBAAA,GAAA,eAAA,aAAA,CAC4D;CACrE,IAAI;CACJ,OAAOA,gBAAAA;CACP,OAAO;CACP,UAAU,CAACC,6BAAAA,gBAAgB;CAC3B,cAAcC,gBAAAA;CACd,QAAQC,gBAAAA;CACR,YAAYC,gBAAAA;CACZ,OAAO,QAAQ;EACb,MAAM,cAAc,IAAI,IAAIH,6BAAAA,gBAAgB;EAC5C,MAAM,YAAY,IAAI,IAAID,gBAAAA,cAAc;EACxC,YAAY,gBAAgBK,gBAAAA,wBAAwB,WAAW,WAAW,CAAC;CAC7E;AACF,CAAC;;;;;;;;;ACGH,MAAa,iBACXC,gBAAAA"}
1
+ {"version":3,"file":"index.cjs","names":["SelectionToken","InteractionToken","FeedbackToken","initialSelectionState","selectionReducer","createSelectionCapability","createTextSelectHandler","SelectionHostToken"],"sources":["../src/selection.plugin.ts","../src/handles.ts","../src/index.ts"],"sourcesContent":["import { definePlugin } from '@embedpdf/core';\nimport { FeedbackToken, InteractionToken } from '@embedpdf/plugin-interaction';\nimport { createSelectionCapability } from './capability';\nimport { createTextSelectHandler } from './handler';\nimport { initialSelectionState, selectionReducer } from './reducer';\nimport { SelectionToken } from './types';\nimport type { SelectionAction, SelectionHostCapability, SelectionState } from './types';\n\n/**\n * Text selection — document-scoped, requires the interaction hub. In `init` it\n * registers ITS pointer handler with the hub; the hub owns the pointer stream\n * and arbitration. Works with `<Stage>` or a standalone `<PageView>` — selection\n * only needs the page coordinate context + the engine's text geometry.\n *\n * Registry/content invalidation is wired inside the capability itself\n * (revision watch + document event subscription), so headless/programmatic\n * use gets the same reconciliation as the full viewer.\n */\nexport const selectionPlugin = () =>\n definePlugin<SelectionState, SelectionAction, SelectionHostCapability>({\n id: 'selection',\n token: SelectionToken,\n scope: 'document',\n requires: [InteractionToken],\n // Platform feedback (haptics) is OPTIONAL: absent in headless setups, the\n // handler simply never buzzes.\n optional: [FeedbackToken],\n initialState: initialSelectionState,\n reduce: selectionReducer,\n capability: createSelectionCapability,\n init: (ctx) => {\n const interaction = ctx.get(InteractionToken);\n const selection = ctx.get(SelectionToken); // our own capability (built before init)\n const feedback = ctx.tryGet(FeedbackToken);\n interaction.registerHandler(\n createTextSelectHandler(selection, interaction, feedback ?? undefined),\n );\n },\n });\n","/**\n * Selection handles — the pure policy half (the `wheel.ts` convention).\n *\n * A handle IS the boundary glyph's leading (or trailing) edge: a segment\n * between two corners of the glyph's ORIENTED cell, with the grab head just\n * beyond the ascent at the start / past the baseline at the end. Bar length,\n * angle, and head all derive from that one segment, so rotated text and\n * rotated pages are carried by construction — the AABB never enters.\n *\n * Everything here is DOM-free and framework-free: geometry is pure given a\n * projector, and the drag session speaks the selection's own gesture verbs\n * (`beginAt`/`extendTo`/`end` — the same ones the pointer handler uses; a\n * handle drag IS a selection gesture, re-anchored). The framework adapters\n * keep only subscriptions and markup; the DOM listener mechanics live in\n * `@embedpdf/web`'s `attachSelectionHandle`.\n *\n * The view dependency is STRUCTURAL (satisfied by five lines over\n * `StageCapability`) so this plugin stays stage-free — selection also runs\n * in stage-less hosts (`PageView`) — and the math tests run against a fake.\n */\nimport { textQuadEdge } from '@embedpdf/core-geometry';\nimport type { Point, TextQuad } from '@embedpdf/core-geometry';\nimport type { PageObjectNumber } from '@embedpdf/core';\n\n/** What handle geometry & drags need from the hosting view. */\nexport interface SelectionHandleView {\n /** Page content point → overlay px. Must be POINT-exact (compose\n * `pageToWorld` with `toScreen`); an AABB projector loses orientation. */\n toOverlay(pon: PageObjectNumber, pt: Point): Point | null;\n /** Overlay px → the page under it, or null over a gap. */\n pageAt(overlay: Point): { pon: PageObjectNumber; point: Point } | null;\n /** Overlay px → a SPECIFIC page's content space, unclamped. */\n pointOnPage(pon: PageObjectNumber, overlay: Point): Point | null;\n}\n\n/** One selection boundary, as the handle needs it (a `SelectionEndpoint` slice). */\nexport interface SelectionHandleEndpoint {\n pon: PageObjectNumber;\n /** The boundary glyph's own oriented cell, page content space. */\n glyphQuad: TextQuad;\n /** Reading direction of its segment (+1 = the frame's +x) — decides which\n * side of the cell is the selection's leading edge. */\n advance: 1 | -1;\n}\n\n/** The gesture verbs a handle drag drives — `SelectionHostCapability` satisfies it. */\nexport interface SelectionHandleTarget {\n beginAt(pon: PageObjectNumber, point: Point): boolean;\n extendTo(pon: PageObjectNumber, point: Point): void;\n end(): void;\n}\n\n// The iOS caret-handle design, one source for every adapter: a thin BAR that\n// is the selection's edge, capped by a screen-constant circle; PAD is the\n// invisible finger padding around the visual.\nexport const HANDLE_HEAD = 12; // px — the circle\nexport const HANDLE_BAR = 2; // px — the caret bar\nexport const HANDLE_PAD = 14; // px — grab padding\n\n/** Tilts within ~0.05° of upright render untransformed (float-noise guard —\n * and the dominant case stays pixel-identical to an axis-aligned box). */\nconst HANDLE_ROT_EPSILON = 0.05;\n\nexport interface SelectionHandleGeom {\n /** The projected edge, ascent corner first, overlay px. */\n bar: { from: Point; to: Point };\n /** The glyph's ink height on screen — zoom scaling comes free. */\n length: number;\n /** Degrees clockwise; 0 when the text is upright on screen. */\n rotation: number;\n /** True within the epsilon of upright: render with NO transform. */\n upright: boolean;\n /** Centre of the grab head, overlay px. */\n head: Point;\n}\n\nconst midpoint = (a: Point, b: Point): Point => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });\n\n/**\n * The handle's geometry for one endpoint, in overlay space. Null when the\n * endpoint's page isn't laid out (or the cell is degenerate) — render nothing.\n */\nexport function selectionHandleGeom(\n view: SelectionHandleView,\n ep: SelectionHandleEndpoint,\n role: 'start' | 'end',\n): SelectionHandleGeom | null {\n // Which SIDE of the cell is this selection's edge is a reading-order\n // question (`advance`), never a geometric one.\n const leading = role === 'start' ? ep.advance > 0 : ep.advance < 0;\n const [upperPage, lowerPage] = textQuadEdge(ep.glyphQuad, leading ? 'start' : 'end');\n const upper = view.toOverlay(ep.pon, upperPage);\n const lower = view.toOverlay(ep.pon, lowerPage);\n if (!upper || !lower) return null;\n const dx = lower.x - upper.x;\n const dy = lower.y - upper.y;\n const length = Math.hypot(dx, dy);\n if (length === 0) return null;\n // 0° when the text is upright: the bar runs straight down the screen.\n const rotation = (Math.atan2(dy, dx) * 180) / Math.PI - 90;\n const upright = Math.abs(rotation) < HANDLE_ROT_EPSILON;\n const ux = dx / length;\n const uy = dy / length;\n const head =\n role === 'start'\n ? { x: upper.x - (ux * HANDLE_HEAD) / 2, y: upper.y - (uy * HANDLE_HEAD) / 2 }\n : { x: lower.x + (ux * HANDLE_HEAD) / 2, y: lower.y + (uy * HANDLE_HEAD) / 2 };\n return { bar: { from: upper, to: lower }, length, rotation, upright, head };\n}\n\nexport interface SelectionHandleDragSession {\n /** Feed the pointer's current overlay position. */\n move(overlay: Point): void;\n /** The pointer released (or was cancelled): settle the gesture. */\n end(): void;\n}\n\n/**\n * One armed handle drag. Dragging a handle extends from the OPPOSITE\n * endpoint: the first movement re-roots the selection gesture at that\n * endpoint's cell centre, and every position then extends toward the pointer\n * — snapping to glyphs and crossing pages exactly like a pointer drag,\n * firing the same change/commit signals. Over a gap the point projects onto\n * the last page hit (unclamped), so the selection keeps tracking instead of\n * freezing at a page edge. `end()` commits only if the drag actually\n * re-rooted; an untouched press settles nothing.\n */\nexport function createSelectionHandleDrag(\n selection: SelectionHandleTarget,\n view: SelectionHandleView,\n opposite: SelectionHandleEndpoint,\n draggedPon: PageObjectNumber,\n): SelectionHandleDragSession {\n // The fixed anchor: the opposite endpoint's cell CENTRE — orientation-free\n // (a parallelogram's diagonal midpoints coincide), so it lands inside the\n // glyph for rotated text too.\n const q = opposite.glyphQuad;\n const anchorPoint = midpoint(midpoint(q.upperStart, q.lowerEnd), midpoint(q.upperEnd, q.lowerStart));\n let begun = false;\n let lastPon = draggedPon;\n return {\n move: (overlay) => {\n if (!begun) {\n if (!selection.beginAt(opposite.pon, anchorPoint)) return;\n begun = true;\n }\n const hit = view.pageAt(overlay);\n if (hit) {\n lastPon = hit.pon;\n selection.extendTo(hit.pon, hit.point);\n } else {\n const p = view.pointOnPage(lastPon, overlay);\n if (p) selection.extendTo(lastPon, p);\n }\n },\n end: () => {\n if (begun) selection.end(); // settle → menu reappears, onCommit fires\n },\n };\n}\n","/**\n * @embedpdf/plugin-selection — text selection over the engine's text geometry.\n *\n * The PUBLIC surface: the plugin factory, the capability token (narrowed to\n * the public lens), and the range/read vocabulary. Selection ranges live in\n * CHARACTER space (half-open `TextRange` — the same space search hits\n * address), geometry needs `doc.text.select`, text extraction needs\n * `doc.text.copy`, and neither permission implies the other.\n *\n * Framework/host plumbing (gesture intents, geometry warming, the reducer,\n * the coordinate seam) lives behind `@embedpdf/plugin-selection/internal`.\n * Clipboard writes live in `@embedpdf/web` — this package is DOM-free.\n */\nimport type { CapabilityToken } from '@embedpdf/core';\nimport { SelectionToken as SelectionHostToken } from './types';\nimport type { SelectionCapability } from './types';\n\nexport { selectionPlugin } from './selection.plugin';\nexport type {\n SelectionCapability,\n SelectionEndpoint,\n SelectionMenuAnchor,\n SelectionRangeInput,\n SelectionSnapshot,\n TextPosition,\n TextRange,\n} from './types';\nexport type { SelectionSegment } from './geometry';\n// Selection-handle policy (the touch affordance): pure geometry + the drag\n// session, consumed by the framework adapters' handle views.\nexport {\n HANDLE_BAR,\n HANDLE_HEAD,\n HANDLE_PAD,\n createSelectionHandleDrag,\n selectionHandleGeom,\n} from './handles';\nexport type {\n SelectionHandleDragSession,\n SelectionHandleEndpoint,\n SelectionHandleGeom,\n SelectionHandleTarget,\n SelectionHandleView,\n} from './handles';\n\n/**\n * The selection capability token, narrowed to the public\n * {@link SelectionCapability} lens. It is the SAME runtime token the plugin\n * provides — the host-only surface (pointer gestures, geometry warming) is\n * reachable only via `@embedpdf/plugin-selection/internal`.\n */\nexport const SelectionToken =\n SelectionHostToken as unknown as CapabilityToken<SelectionCapability>;\n"],"mappings":";;;;;;;;;;;;;;;;AAkBA,MAAa,yBAAA,GAAA,eAAA,aAAA,CAC4D;CACrE,IAAI;CACJ,OAAOA,gBAAAA;CACP,OAAO;CACP,UAAU,CAACC,6BAAAA,gBAAgB;CAG3B,UAAU,CAACC,6BAAAA,aAAa;CACxB,cAAcC,gBAAAA;CACd,QAAQC,gBAAAA;CACR,YAAYC,gBAAAA;CACZ,OAAO,QAAQ;EACb,MAAM,cAAc,IAAI,IAAIJ,6BAAAA,gBAAgB;EAC5C,MAAM,YAAY,IAAI,IAAID,gBAAAA,cAAc;EACxC,MAAM,WAAW,IAAI,OAAOE,6BAAAA,aAAa;EACzC,YAAY,gBACVI,gBAAAA,wBAAwB,WAAW,aAAa,YAAY,KAAA,CAAS,CACvE;CACF;AACF,CAAC;;;;;;;;;;;;;;;;;;;;;;;ACiBH,MAAa,cAAc;AAC3B,MAAa,aAAa;AAC1B,MAAa,aAAa;;;AAI1B,MAAM,qBAAqB;AAe3B,MAAM,YAAY,GAAU,OAAqB;CAAE,IAAI,EAAE,IAAI,EAAE,KAAK;CAAG,IAAI,EAAE,IAAI,EAAE,KAAK;AAAE;;;;;AAM1F,SAAgB,oBACd,MACA,IACA,MAC4B;CAG5B,MAAM,UAAU,SAAS,UAAU,GAAG,UAAU,IAAI,GAAG,UAAU;CACjE,MAAM,CAAC,WAAW,cAAA,GAAA,wBAAA,aAAA,CAA0B,GAAG,WAAW,UAAU,UAAU,KAAK;CACnF,MAAM,QAAQ,KAAK,UAAU,GAAG,KAAK,SAAS;CAC9C,MAAM,QAAQ,KAAK,UAAU,GAAG,KAAK,SAAS;CAC9C,IAAI,CAAC,SAAS,CAAC,OAAO,OAAO;CAC7B,MAAM,KAAK,MAAM,IAAI,MAAM;CAC3B,MAAM,KAAK,MAAM,IAAI,MAAM;CAC3B,MAAM,SAAS,KAAK,MAAM,IAAI,EAAE;CAChC,IAAI,WAAW,GAAG,OAAO;CAEzB,MAAM,WAAY,KAAK,MAAM,IAAI,EAAE,IAAI,MAAO,KAAK,KAAK;CACxD,MAAM,UAAU,KAAK,IAAI,QAAQ,IAAI;CACrC,MAAM,KAAK,KAAK;CAChB,MAAM,KAAK,KAAK;CAChB,MAAM,OACJ,SAAS,UACL;EAAE,GAAG,MAAM,IAAK,KAAA,KAAoB;EAAG,GAAG,MAAM,IAAK,KAAA,KAAoB;CAAE,IAC3E;EAAE,GAAG,MAAM,IAAK,KAAA,KAAoB;EAAG,GAAG,MAAM,IAAK,KAAA,KAAoB;CAAE;CACjF,OAAO;EAAE,KAAK;GAAE,MAAM;GAAO,IAAI;EAAM;EAAG;EAAQ;EAAU;EAAS;CAAK;AAC5E;;;;;;;;;;;AAmBA,SAAgB,0BACd,WACA,MACA,UACA,YAC4B;CAI5B,MAAM,IAAI,SAAS;CACnB,MAAM,cAAc,SAAS,SAAS,EAAE,YAAY,EAAE,QAAQ,GAAG,SAAS,EAAE,UAAU,EAAE,UAAU,CAAC;CACnG,IAAI,QAAQ;CACZ,IAAI,UAAU;CACd,OAAO;EACL,OAAO,YAAY;GACjB,IAAI,CAAC,OAAO;IACV,IAAI,CAAC,UAAU,QAAQ,SAAS,KAAK,WAAW,GAAG;IACnD,QAAQ;GACV;GACA,MAAM,MAAM,KAAK,OAAO,OAAO;GAC/B,IAAI,KAAK;IACP,UAAU,IAAI;IACd,UAAU,SAAS,IAAI,KAAK,IAAI,KAAK;GACvC,OAAO;IACL,MAAM,IAAI,KAAK,YAAY,SAAS,OAAO;IAC3C,IAAI,GAAG,UAAU,SAAS,SAAS,CAAC;GACtC;EACF;EACA,WAAW;GACT,IAAI,OAAO,UAAU,IAAI;EAC3B;CACF;AACF;;;;;;;;;AC5GA,MAAa,iBACXC,gBAAAA"}
package/dist/index.d.cts CHANGED
@@ -1,5 +1,6 @@
1
- import { a as SelectionHostCapability, c as SelectionRangeInput, f as TextPosition, h as SelectionSegment, i as SelectionEndpoint, l as SelectionSnapshot, n as SelectionAction, o as SelectionMenuAnchor, p as TextRange, r as SelectionCapability, u as SelectionState } from "./types-DtWaZIsm.cjs";
2
- import { CapabilityToken } from "@embedpdf/core";
1
+ import { a as SelectionHostCapability, c as SelectionRangeInput, f as TextPosition, h as SelectionSegment, i as SelectionEndpoint, l as SelectionSnapshot, n as SelectionAction, o as SelectionMenuAnchor, p as TextRange, r as SelectionCapability, u as SelectionState } from "./types-BCPuB3iZ.cjs";
2
+ import { CapabilityToken, PageObjectNumber } from "@embedpdf/core";
3
+ import { Point, TextQuad } from "@embedpdf/core-geometry";
3
4
  //#region src/selection.plugin.d.ts
4
5
  /**
5
6
  * Text selection — document-scoped, requires the interaction hub. In `init` it
@@ -13,6 +14,76 @@ import { CapabilityToken } from "@embedpdf/core";
13
14
  */
14
15
  declare const selectionPlugin: () => import("@embedpdf/core").PluginDef<SelectionState, SelectionAction, SelectionHostCapability>;
15
16
  //#endregion
17
+ //#region src/handles.d.ts
18
+ /** What handle geometry & drags need from the hosting view. */
19
+ interface SelectionHandleView {
20
+ /** Page content point → overlay px. Must be POINT-exact (compose
21
+ * `pageToWorld` with `toScreen`); an AABB projector loses orientation. */
22
+ toOverlay(pon: PageObjectNumber, pt: Point): Point | null;
23
+ /** Overlay px → the page under it, or null over a gap. */
24
+ pageAt(overlay: Point): {
25
+ pon: PageObjectNumber;
26
+ point: Point;
27
+ } | null;
28
+ /** Overlay px → a SPECIFIC page's content space, unclamped. */
29
+ pointOnPage(pon: PageObjectNumber, overlay: Point): Point | null;
30
+ }
31
+ /** One selection boundary, as the handle needs it (a `SelectionEndpoint` slice). */
32
+ interface SelectionHandleEndpoint {
33
+ pon: PageObjectNumber;
34
+ /** The boundary glyph's own oriented cell, page content space. */
35
+ glyphQuad: TextQuad;
36
+ /** Reading direction of its segment (+1 = the frame's +x) — decides which
37
+ * side of the cell is the selection's leading edge. */
38
+ advance: 1 | -1;
39
+ }
40
+ /** The gesture verbs a handle drag drives — `SelectionHostCapability` satisfies it. */
41
+ interface SelectionHandleTarget {
42
+ beginAt(pon: PageObjectNumber, point: Point): boolean;
43
+ extendTo(pon: PageObjectNumber, point: Point): void;
44
+ end(): void;
45
+ }
46
+ declare const HANDLE_HEAD = 12;
47
+ declare const HANDLE_BAR = 2;
48
+ declare const HANDLE_PAD = 14;
49
+ interface SelectionHandleGeom {
50
+ /** The projected edge, ascent corner first, overlay px. */
51
+ bar: {
52
+ from: Point;
53
+ to: Point;
54
+ };
55
+ /** The glyph's ink height on screen — zoom scaling comes free. */
56
+ length: number;
57
+ /** Degrees clockwise; 0 when the text is upright on screen. */
58
+ rotation: number;
59
+ /** True within the epsilon of upright: render with NO transform. */
60
+ upright: boolean;
61
+ /** Centre of the grab head, overlay px. */
62
+ head: Point;
63
+ }
64
+ /**
65
+ * The handle's geometry for one endpoint, in overlay space. Null when the
66
+ * endpoint's page isn't laid out (or the cell is degenerate) — render nothing.
67
+ */
68
+ declare function selectionHandleGeom(view: SelectionHandleView, ep: SelectionHandleEndpoint, role: 'start' | 'end'): SelectionHandleGeom | null;
69
+ interface SelectionHandleDragSession {
70
+ /** Feed the pointer's current overlay position. */
71
+ move(overlay: Point): void;
72
+ /** The pointer released (or was cancelled): settle the gesture. */
73
+ end(): void;
74
+ }
75
+ /**
76
+ * One armed handle drag. Dragging a handle extends from the OPPOSITE
77
+ * endpoint: the first movement re-roots the selection gesture at that
78
+ * endpoint's cell centre, and every position then extends toward the pointer
79
+ * — snapping to glyphs and crossing pages exactly like a pointer drag,
80
+ * firing the same change/commit signals. Over a gap the point projects onto
81
+ * the last page hit (unclamped), so the selection keeps tracking instead of
82
+ * freezing at a page edge. `end()` commits only if the drag actually
83
+ * re-rooted; an untouched press settles nothing.
84
+ */
85
+ declare function createSelectionHandleDrag(selection: SelectionHandleTarget, view: SelectionHandleView, opposite: SelectionHandleEndpoint, draggedPon: PageObjectNumber): SelectionHandleDragSession;
86
+ //#endregion
16
87
  //#region src/index.d.ts
17
88
  /**
18
89
  * The selection capability token, narrowed to the public
@@ -22,5 +93,5 @@ declare const selectionPlugin: () => import("@embedpdf/core").PluginDef<Selectio
22
93
  */
23
94
  declare const SelectionToken: CapabilityToken<SelectionCapability>;
24
95
  //#endregion
25
- export { type SelectionCapability, type SelectionEndpoint, type SelectionMenuAnchor, type SelectionRangeInput, type SelectionSegment, type SelectionSnapshot, SelectionToken, type TextPosition, type TextRange, selectionPlugin };
96
+ export { HANDLE_BAR, HANDLE_HEAD, HANDLE_PAD, type SelectionCapability, type SelectionEndpoint, type SelectionHandleDragSession, type SelectionHandleEndpoint, type SelectionHandleGeom, type SelectionHandleTarget, type SelectionHandleView, type SelectionMenuAnchor, type SelectionRangeInput, type SelectionSegment, type SelectionSnapshot, SelectionToken, type TextPosition, type TextRange, createSelectionHandleDrag, selectionHandleGeom, selectionPlugin };
26
97
  //# sourceMappingURL=index.d.cts.map
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
- import { a as SelectionHostCapability, c as SelectionRangeInput, f as TextPosition, h as SelectionSegment, i as SelectionEndpoint, l as SelectionSnapshot, n as SelectionAction, o as SelectionMenuAnchor, p as TextRange, r as SelectionCapability, u as SelectionState } from "./types-DtWaZIsm.js";
2
- import { CapabilityToken } from "@embedpdf/core";
1
+ import { a as SelectionHostCapability, c as SelectionRangeInput, f as TextPosition, h as SelectionSegment, i as SelectionEndpoint, l as SelectionSnapshot, n as SelectionAction, o as SelectionMenuAnchor, p as TextRange, r as SelectionCapability, u as SelectionState } from "./types-BCPuB3iZ.js";
2
+ import { CapabilityToken, PageObjectNumber } from "@embedpdf/core";
3
+ import { Point, TextQuad } from "@embedpdf/core-geometry";
3
4
  //#region src/selection.plugin.d.ts
4
5
  /**
5
6
  * Text selection — document-scoped, requires the interaction hub. In `init` it
@@ -13,6 +14,76 @@ import { CapabilityToken } from "@embedpdf/core";
13
14
  */
14
15
  declare const selectionPlugin: () => import("@embedpdf/core").PluginDef<SelectionState, SelectionAction, SelectionHostCapability>;
15
16
  //#endregion
17
+ //#region src/handles.d.ts
18
+ /** What handle geometry & drags need from the hosting view. */
19
+ interface SelectionHandleView {
20
+ /** Page content point → overlay px. Must be POINT-exact (compose
21
+ * `pageToWorld` with `toScreen`); an AABB projector loses orientation. */
22
+ toOverlay(pon: PageObjectNumber, pt: Point): Point | null;
23
+ /** Overlay px → the page under it, or null over a gap. */
24
+ pageAt(overlay: Point): {
25
+ pon: PageObjectNumber;
26
+ point: Point;
27
+ } | null;
28
+ /** Overlay px → a SPECIFIC page's content space, unclamped. */
29
+ pointOnPage(pon: PageObjectNumber, overlay: Point): Point | null;
30
+ }
31
+ /** One selection boundary, as the handle needs it (a `SelectionEndpoint` slice). */
32
+ interface SelectionHandleEndpoint {
33
+ pon: PageObjectNumber;
34
+ /** The boundary glyph's own oriented cell, page content space. */
35
+ glyphQuad: TextQuad;
36
+ /** Reading direction of its segment (+1 = the frame's +x) — decides which
37
+ * side of the cell is the selection's leading edge. */
38
+ advance: 1 | -1;
39
+ }
40
+ /** The gesture verbs a handle drag drives — `SelectionHostCapability` satisfies it. */
41
+ interface SelectionHandleTarget {
42
+ beginAt(pon: PageObjectNumber, point: Point): boolean;
43
+ extendTo(pon: PageObjectNumber, point: Point): void;
44
+ end(): void;
45
+ }
46
+ declare const HANDLE_HEAD = 12;
47
+ declare const HANDLE_BAR = 2;
48
+ declare const HANDLE_PAD = 14;
49
+ interface SelectionHandleGeom {
50
+ /** The projected edge, ascent corner first, overlay px. */
51
+ bar: {
52
+ from: Point;
53
+ to: Point;
54
+ };
55
+ /** The glyph's ink height on screen — zoom scaling comes free. */
56
+ length: number;
57
+ /** Degrees clockwise; 0 when the text is upright on screen. */
58
+ rotation: number;
59
+ /** True within the epsilon of upright: render with NO transform. */
60
+ upright: boolean;
61
+ /** Centre of the grab head, overlay px. */
62
+ head: Point;
63
+ }
64
+ /**
65
+ * The handle's geometry for one endpoint, in overlay space. Null when the
66
+ * endpoint's page isn't laid out (or the cell is degenerate) — render nothing.
67
+ */
68
+ declare function selectionHandleGeom(view: SelectionHandleView, ep: SelectionHandleEndpoint, role: 'start' | 'end'): SelectionHandleGeom | null;
69
+ interface SelectionHandleDragSession {
70
+ /** Feed the pointer's current overlay position. */
71
+ move(overlay: Point): void;
72
+ /** The pointer released (or was cancelled): settle the gesture. */
73
+ end(): void;
74
+ }
75
+ /**
76
+ * One armed handle drag. Dragging a handle extends from the OPPOSITE
77
+ * endpoint: the first movement re-roots the selection gesture at that
78
+ * endpoint's cell centre, and every position then extends toward the pointer
79
+ * — snapping to glyphs and crossing pages exactly like a pointer drag,
80
+ * firing the same change/commit signals. Over a gap the point projects onto
81
+ * the last page hit (unclamped), so the selection keeps tracking instead of
82
+ * freezing at a page edge. `end()` commits only if the drag actually
83
+ * re-rooted; an untouched press settles nothing.
84
+ */
85
+ declare function createSelectionHandleDrag(selection: SelectionHandleTarget, view: SelectionHandleView, opposite: SelectionHandleEndpoint, draggedPon: PageObjectNumber): SelectionHandleDragSession;
86
+ //#endregion
16
87
  //#region src/index.d.ts
17
88
  /**
18
89
  * The selection capability token, narrowed to the public
@@ -22,5 +93,5 @@ declare const selectionPlugin: () => import("@embedpdf/core").PluginDef<Selectio
22
93
  */
23
94
  declare const SelectionToken: CapabilityToken<SelectionCapability>;
24
95
  //#endregion
25
- export { type SelectionCapability, type SelectionEndpoint, type SelectionMenuAnchor, type SelectionRangeInput, type SelectionSegment, type SelectionSnapshot, SelectionToken, type TextPosition, type TextRange, selectionPlugin };
96
+ export { HANDLE_BAR, HANDLE_HEAD, HANDLE_PAD, type SelectionCapability, type SelectionEndpoint, type SelectionHandleDragSession, type SelectionHandleEndpoint, type SelectionHandleGeom, type SelectionHandleTarget, type SelectionHandleView, type SelectionMenuAnchor, type SelectionRangeInput, type SelectionSegment, type SelectionSnapshot, SelectionToken, type TextPosition, type TextRange, createSelectionHandleDrag, selectionHandleGeom, selectionPlugin };
26
97
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
- import { i as createSelectionCapability, l as SelectionToken$1, n as selectionReducer, r as createTextSelectHandler, t as initialSelectionState } from "./reducer-CZG2TZgo.js";
1
+ import { i as createSelectionCapability, l as SelectionToken$1, n as selectionReducer, r as createTextSelectHandler, t as initialSelectionState } from "./reducer-BhLvJwVe.js";
2
2
  import { definePlugin } from "@embedpdf/core";
3
- import { InteractionToken } from "@embedpdf/plugin-interaction";
3
+ import { FeedbackToken, InteractionToken } from "@embedpdf/plugin-interaction";
4
+ import { textQuadEdge } from "@embedpdf/core-geometry";
4
5
  //#region src/selection.plugin.ts
5
6
  /**
6
7
  * Text selection — document-scoped, requires the interaction hub. In `init` it
@@ -17,16 +18,121 @@ const selectionPlugin = () => definePlugin({
17
18
  token: SelectionToken$1,
18
19
  scope: "document",
19
20
  requires: [InteractionToken],
21
+ optional: [FeedbackToken],
20
22
  initialState: initialSelectionState,
21
23
  reduce: selectionReducer,
22
24
  capability: createSelectionCapability,
23
25
  init: (ctx) => {
24
26
  const interaction = ctx.get(InteractionToken);
25
27
  const selection = ctx.get(SelectionToken$1);
26
- interaction.registerHandler(createTextSelectHandler(selection, interaction));
28
+ const feedback = ctx.tryGet(FeedbackToken);
29
+ interaction.registerHandler(createTextSelectHandler(selection, interaction, feedback ?? void 0));
27
30
  }
28
31
  });
29
32
  //#endregion
33
+ //#region src/handles.ts
34
+ /**
35
+ * Selection handles — the pure policy half (the `wheel.ts` convention).
36
+ *
37
+ * A handle IS the boundary glyph's leading (or trailing) edge: a segment
38
+ * between two corners of the glyph's ORIENTED cell, with the grab head just
39
+ * beyond the ascent at the start / past the baseline at the end. Bar length,
40
+ * angle, and head all derive from that one segment, so rotated text and
41
+ * rotated pages are carried by construction — the AABB never enters.
42
+ *
43
+ * Everything here is DOM-free and framework-free: geometry is pure given a
44
+ * projector, and the drag session speaks the selection's own gesture verbs
45
+ * (`beginAt`/`extendTo`/`end` — the same ones the pointer handler uses; a
46
+ * handle drag IS a selection gesture, re-anchored). The framework adapters
47
+ * keep only subscriptions and markup; the DOM listener mechanics live in
48
+ * `@embedpdf/web`'s `attachSelectionHandle`.
49
+ *
50
+ * The view dependency is STRUCTURAL (satisfied by five lines over
51
+ * `StageCapability`) so this plugin stays stage-free — selection also runs
52
+ * in stage-less hosts (`PageView`) — and the math tests run against a fake.
53
+ */
54
+ const HANDLE_HEAD = 12;
55
+ const HANDLE_BAR = 2;
56
+ const HANDLE_PAD = 14;
57
+ /** Tilts within ~0.05° of upright render untransformed (float-noise guard —
58
+ * and the dominant case stays pixel-identical to an axis-aligned box). */
59
+ const HANDLE_ROT_EPSILON = .05;
60
+ const midpoint = (a, b) => ({
61
+ x: (a.x + b.x) / 2,
62
+ y: (a.y + b.y) / 2
63
+ });
64
+ /**
65
+ * The handle's geometry for one endpoint, in overlay space. Null when the
66
+ * endpoint's page isn't laid out (or the cell is degenerate) — render nothing.
67
+ */
68
+ function selectionHandleGeom(view, ep, role) {
69
+ const leading = role === "start" ? ep.advance > 0 : ep.advance < 0;
70
+ const [upperPage, lowerPage] = textQuadEdge(ep.glyphQuad, leading ? "start" : "end");
71
+ const upper = view.toOverlay(ep.pon, upperPage);
72
+ const lower = view.toOverlay(ep.pon, lowerPage);
73
+ if (!upper || !lower) return null;
74
+ const dx = lower.x - upper.x;
75
+ const dy = lower.y - upper.y;
76
+ const length = Math.hypot(dx, dy);
77
+ if (length === 0) return null;
78
+ const rotation = Math.atan2(dy, dx) * 180 / Math.PI - 90;
79
+ const upright = Math.abs(rotation) < HANDLE_ROT_EPSILON;
80
+ const ux = dx / length;
81
+ const uy = dy / length;
82
+ const head = role === "start" ? {
83
+ x: upper.x - ux * 12 / 2,
84
+ y: upper.y - uy * 12 / 2
85
+ } : {
86
+ x: lower.x + ux * 12 / 2,
87
+ y: lower.y + uy * 12 / 2
88
+ };
89
+ return {
90
+ bar: {
91
+ from: upper,
92
+ to: lower
93
+ },
94
+ length,
95
+ rotation,
96
+ upright,
97
+ head
98
+ };
99
+ }
100
+ /**
101
+ * One armed handle drag. Dragging a handle extends from the OPPOSITE
102
+ * endpoint: the first movement re-roots the selection gesture at that
103
+ * endpoint's cell centre, and every position then extends toward the pointer
104
+ * — snapping to glyphs and crossing pages exactly like a pointer drag,
105
+ * firing the same change/commit signals. Over a gap the point projects onto
106
+ * the last page hit (unclamped), so the selection keeps tracking instead of
107
+ * freezing at a page edge. `end()` commits only if the drag actually
108
+ * re-rooted; an untouched press settles nothing.
109
+ */
110
+ function createSelectionHandleDrag(selection, view, opposite, draggedPon) {
111
+ const q = opposite.glyphQuad;
112
+ const anchorPoint = midpoint(midpoint(q.upperStart, q.lowerEnd), midpoint(q.upperEnd, q.lowerStart));
113
+ let begun = false;
114
+ let lastPon = draggedPon;
115
+ return {
116
+ move: (overlay) => {
117
+ if (!begun) {
118
+ if (!selection.beginAt(opposite.pon, anchorPoint)) return;
119
+ begun = true;
120
+ }
121
+ const hit = view.pageAt(overlay);
122
+ if (hit) {
123
+ lastPon = hit.pon;
124
+ selection.extendTo(hit.pon, hit.point);
125
+ } else {
126
+ const p = view.pointOnPage(lastPon, overlay);
127
+ if (p) selection.extendTo(lastPon, p);
128
+ }
129
+ },
130
+ end: () => {
131
+ if (begun) selection.end();
132
+ }
133
+ };
134
+ }
135
+ //#endregion
30
136
  //#region src/index.ts
31
137
  /**
32
138
  * The selection capability token, narrowed to the public
@@ -36,6 +142,6 @@ const selectionPlugin = () => definePlugin({
36
142
  */
37
143
  const SelectionToken = SelectionToken$1;
38
144
  //#endregion
39
- export { SelectionToken, selectionPlugin };
145
+ export { HANDLE_BAR, HANDLE_HEAD, HANDLE_PAD, SelectionToken, createSelectionHandleDrag, selectionHandleGeom, selectionPlugin };
40
146
 
41
147
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["SelectionToken","SelectionHostToken"],"sources":["../src/selection.plugin.ts","../src/index.ts"],"sourcesContent":["import { definePlugin } from '@embedpdf/core';\nimport { InteractionToken } from '@embedpdf/plugin-interaction';\nimport { createSelectionCapability } from './capability';\nimport { createTextSelectHandler } from './handler';\nimport { initialSelectionState, selectionReducer } from './reducer';\nimport { SelectionToken } from './types';\nimport type { SelectionAction, SelectionHostCapability, SelectionState } from './types';\n\n/**\n * Text selection — document-scoped, requires the interaction hub. In `init` it\n * registers ITS pointer handler with the hub; the hub owns the pointer stream\n * and arbitration. Works with `<Stage>` or a standalone `<PageView>` — selection\n * only needs the page coordinate context + the engine's text geometry.\n *\n * Registry/content invalidation is wired inside the capability itself\n * (revision watch + document event subscription), so headless/programmatic\n * use gets the same reconciliation as the full viewer.\n */\nexport const selectionPlugin = () =>\n definePlugin<SelectionState, SelectionAction, SelectionHostCapability>({\n id: 'selection',\n token: SelectionToken,\n scope: 'document',\n requires: [InteractionToken],\n initialState: initialSelectionState,\n reduce: selectionReducer,\n capability: createSelectionCapability,\n init: (ctx) => {\n const interaction = ctx.get(InteractionToken);\n const selection = ctx.get(SelectionToken); // our own capability (built before init)\n interaction.registerHandler(createTextSelectHandler(selection, interaction));\n },\n });\n","/**\n * @embedpdf/plugin-selection — text selection over the engine's text geometry.\n *\n * The PUBLIC surface: the plugin factory, the capability token (narrowed to\n * the public lens), and the range/read vocabulary. Selection ranges live in\n * CHARACTER space (half-open `TextRange` — the same space search hits\n * address), geometry needs `doc.text.select`, text extraction needs\n * `doc.text.copy`, and neither permission implies the other.\n *\n * Framework/host plumbing (gesture intents, geometry warming, the reducer,\n * the coordinate seam) lives behind `@embedpdf/plugin-selection/internal`.\n * Clipboard writes live in `@embedpdf/web` — this package is DOM-free.\n */\nimport type { CapabilityToken } from '@embedpdf/core';\nimport { SelectionToken as SelectionHostToken } from './types';\nimport type { SelectionCapability } from './types';\n\nexport { selectionPlugin } from './selection.plugin';\nexport type {\n SelectionCapability,\n SelectionEndpoint,\n SelectionMenuAnchor,\n SelectionRangeInput,\n SelectionSnapshot,\n TextPosition,\n TextRange,\n} from './types';\nexport type { SelectionSegment } from './geometry';\n\n/**\n * The selection capability token, narrowed to the public\n * {@link SelectionCapability} lens. It is the SAME runtime token the plugin\n * provides — the host-only surface (pointer gestures, geometry warming) is\n * reachable only via `@embedpdf/plugin-selection/internal`.\n */\nexport const SelectionToken =\n SelectionHostToken as unknown as CapabilityToken<SelectionCapability>;\n"],"mappings":";;;;;;;;;;;;;;AAkBA,MAAa,wBACX,aAAuE;CACrE,IAAI;CACJ,OAAOA;CACP,OAAO;CACP,UAAU,CAAC,gBAAgB;CAC3B,cAAc;CACd,QAAQ;CACR,YAAY;CACZ,OAAO,QAAQ;EACb,MAAM,cAAc,IAAI,IAAI,gBAAgB;EAC5C,MAAM,YAAY,IAAI,IAAIA,gBAAc;EACxC,YAAY,gBAAgB,wBAAwB,WAAW,WAAW,CAAC;CAC7E;AACF,CAAC;;;;;;;;;ACGH,MAAa,iBACXC"}
1
+ {"version":3,"file":"index.js","names":["SelectionToken","SelectionHostToken"],"sources":["../src/selection.plugin.ts","../src/handles.ts","../src/index.ts"],"sourcesContent":["import { definePlugin } from '@embedpdf/core';\nimport { FeedbackToken, InteractionToken } from '@embedpdf/plugin-interaction';\nimport { createSelectionCapability } from './capability';\nimport { createTextSelectHandler } from './handler';\nimport { initialSelectionState, selectionReducer } from './reducer';\nimport { SelectionToken } from './types';\nimport type { SelectionAction, SelectionHostCapability, SelectionState } from './types';\n\n/**\n * Text selection — document-scoped, requires the interaction hub. In `init` it\n * registers ITS pointer handler with the hub; the hub owns the pointer stream\n * and arbitration. Works with `<Stage>` or a standalone `<PageView>` — selection\n * only needs the page coordinate context + the engine's text geometry.\n *\n * Registry/content invalidation is wired inside the capability itself\n * (revision watch + document event subscription), so headless/programmatic\n * use gets the same reconciliation as the full viewer.\n */\nexport const selectionPlugin = () =>\n definePlugin<SelectionState, SelectionAction, SelectionHostCapability>({\n id: 'selection',\n token: SelectionToken,\n scope: 'document',\n requires: [InteractionToken],\n // Platform feedback (haptics) is OPTIONAL: absent in headless setups, the\n // handler simply never buzzes.\n optional: [FeedbackToken],\n initialState: initialSelectionState,\n reduce: selectionReducer,\n capability: createSelectionCapability,\n init: (ctx) => {\n const interaction = ctx.get(InteractionToken);\n const selection = ctx.get(SelectionToken); // our own capability (built before init)\n const feedback = ctx.tryGet(FeedbackToken);\n interaction.registerHandler(\n createTextSelectHandler(selection, interaction, feedback ?? undefined),\n );\n },\n });\n","/**\n * Selection handles — the pure policy half (the `wheel.ts` convention).\n *\n * A handle IS the boundary glyph's leading (or trailing) edge: a segment\n * between two corners of the glyph's ORIENTED cell, with the grab head just\n * beyond the ascent at the start / past the baseline at the end. Bar length,\n * angle, and head all derive from that one segment, so rotated text and\n * rotated pages are carried by construction — the AABB never enters.\n *\n * Everything here is DOM-free and framework-free: geometry is pure given a\n * projector, and the drag session speaks the selection's own gesture verbs\n * (`beginAt`/`extendTo`/`end` — the same ones the pointer handler uses; a\n * handle drag IS a selection gesture, re-anchored). The framework adapters\n * keep only subscriptions and markup; the DOM listener mechanics live in\n * `@embedpdf/web`'s `attachSelectionHandle`.\n *\n * The view dependency is STRUCTURAL (satisfied by five lines over\n * `StageCapability`) so this plugin stays stage-free — selection also runs\n * in stage-less hosts (`PageView`) — and the math tests run against a fake.\n */\nimport { textQuadEdge } from '@embedpdf/core-geometry';\nimport type { Point, TextQuad } from '@embedpdf/core-geometry';\nimport type { PageObjectNumber } from '@embedpdf/core';\n\n/** What handle geometry & drags need from the hosting view. */\nexport interface SelectionHandleView {\n /** Page content point → overlay px. Must be POINT-exact (compose\n * `pageToWorld` with `toScreen`); an AABB projector loses orientation. */\n toOverlay(pon: PageObjectNumber, pt: Point): Point | null;\n /** Overlay px → the page under it, or null over a gap. */\n pageAt(overlay: Point): { pon: PageObjectNumber; point: Point } | null;\n /** Overlay px → a SPECIFIC page's content space, unclamped. */\n pointOnPage(pon: PageObjectNumber, overlay: Point): Point | null;\n}\n\n/** One selection boundary, as the handle needs it (a `SelectionEndpoint` slice). */\nexport interface SelectionHandleEndpoint {\n pon: PageObjectNumber;\n /** The boundary glyph's own oriented cell, page content space. */\n glyphQuad: TextQuad;\n /** Reading direction of its segment (+1 = the frame's +x) — decides which\n * side of the cell is the selection's leading edge. */\n advance: 1 | -1;\n}\n\n/** The gesture verbs a handle drag drives — `SelectionHostCapability` satisfies it. */\nexport interface SelectionHandleTarget {\n beginAt(pon: PageObjectNumber, point: Point): boolean;\n extendTo(pon: PageObjectNumber, point: Point): void;\n end(): void;\n}\n\n// The iOS caret-handle design, one source for every adapter: a thin BAR that\n// is the selection's edge, capped by a screen-constant circle; PAD is the\n// invisible finger padding around the visual.\nexport const HANDLE_HEAD = 12; // px — the circle\nexport const HANDLE_BAR = 2; // px — the caret bar\nexport const HANDLE_PAD = 14; // px — grab padding\n\n/** Tilts within ~0.05° of upright render untransformed (float-noise guard —\n * and the dominant case stays pixel-identical to an axis-aligned box). */\nconst HANDLE_ROT_EPSILON = 0.05;\n\nexport interface SelectionHandleGeom {\n /** The projected edge, ascent corner first, overlay px. */\n bar: { from: Point; to: Point };\n /** The glyph's ink height on screen — zoom scaling comes free. */\n length: number;\n /** Degrees clockwise; 0 when the text is upright on screen. */\n rotation: number;\n /** True within the epsilon of upright: render with NO transform. */\n upright: boolean;\n /** Centre of the grab head, overlay px. */\n head: Point;\n}\n\nconst midpoint = (a: Point, b: Point): Point => ({ x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 });\n\n/**\n * The handle's geometry for one endpoint, in overlay space. Null when the\n * endpoint's page isn't laid out (or the cell is degenerate) — render nothing.\n */\nexport function selectionHandleGeom(\n view: SelectionHandleView,\n ep: SelectionHandleEndpoint,\n role: 'start' | 'end',\n): SelectionHandleGeom | null {\n // Which SIDE of the cell is this selection's edge is a reading-order\n // question (`advance`), never a geometric one.\n const leading = role === 'start' ? ep.advance > 0 : ep.advance < 0;\n const [upperPage, lowerPage] = textQuadEdge(ep.glyphQuad, leading ? 'start' : 'end');\n const upper = view.toOverlay(ep.pon, upperPage);\n const lower = view.toOverlay(ep.pon, lowerPage);\n if (!upper || !lower) return null;\n const dx = lower.x - upper.x;\n const dy = lower.y - upper.y;\n const length = Math.hypot(dx, dy);\n if (length === 0) return null;\n // 0° when the text is upright: the bar runs straight down the screen.\n const rotation = (Math.atan2(dy, dx) * 180) / Math.PI - 90;\n const upright = Math.abs(rotation) < HANDLE_ROT_EPSILON;\n const ux = dx / length;\n const uy = dy / length;\n const head =\n role === 'start'\n ? { x: upper.x - (ux * HANDLE_HEAD) / 2, y: upper.y - (uy * HANDLE_HEAD) / 2 }\n : { x: lower.x + (ux * HANDLE_HEAD) / 2, y: lower.y + (uy * HANDLE_HEAD) / 2 };\n return { bar: { from: upper, to: lower }, length, rotation, upright, head };\n}\n\nexport interface SelectionHandleDragSession {\n /** Feed the pointer's current overlay position. */\n move(overlay: Point): void;\n /** The pointer released (or was cancelled): settle the gesture. */\n end(): void;\n}\n\n/**\n * One armed handle drag. Dragging a handle extends from the OPPOSITE\n * endpoint: the first movement re-roots the selection gesture at that\n * endpoint's cell centre, and every position then extends toward the pointer\n * — snapping to glyphs and crossing pages exactly like a pointer drag,\n * firing the same change/commit signals. Over a gap the point projects onto\n * the last page hit (unclamped), so the selection keeps tracking instead of\n * freezing at a page edge. `end()` commits only if the drag actually\n * re-rooted; an untouched press settles nothing.\n */\nexport function createSelectionHandleDrag(\n selection: SelectionHandleTarget,\n view: SelectionHandleView,\n opposite: SelectionHandleEndpoint,\n draggedPon: PageObjectNumber,\n): SelectionHandleDragSession {\n // The fixed anchor: the opposite endpoint's cell CENTRE — orientation-free\n // (a parallelogram's diagonal midpoints coincide), so it lands inside the\n // glyph for rotated text too.\n const q = opposite.glyphQuad;\n const anchorPoint = midpoint(midpoint(q.upperStart, q.lowerEnd), midpoint(q.upperEnd, q.lowerStart));\n let begun = false;\n let lastPon = draggedPon;\n return {\n move: (overlay) => {\n if (!begun) {\n if (!selection.beginAt(opposite.pon, anchorPoint)) return;\n begun = true;\n }\n const hit = view.pageAt(overlay);\n if (hit) {\n lastPon = hit.pon;\n selection.extendTo(hit.pon, hit.point);\n } else {\n const p = view.pointOnPage(lastPon, overlay);\n if (p) selection.extendTo(lastPon, p);\n }\n },\n end: () => {\n if (begun) selection.end(); // settle → menu reappears, onCommit fires\n },\n };\n}\n","/**\n * @embedpdf/plugin-selection — text selection over the engine's text geometry.\n *\n * The PUBLIC surface: the plugin factory, the capability token (narrowed to\n * the public lens), and the range/read vocabulary. Selection ranges live in\n * CHARACTER space (half-open `TextRange` — the same space search hits\n * address), geometry needs `doc.text.select`, text extraction needs\n * `doc.text.copy`, and neither permission implies the other.\n *\n * Framework/host plumbing (gesture intents, geometry warming, the reducer,\n * the coordinate seam) lives behind `@embedpdf/plugin-selection/internal`.\n * Clipboard writes live in `@embedpdf/web` — this package is DOM-free.\n */\nimport type { CapabilityToken } from '@embedpdf/core';\nimport { SelectionToken as SelectionHostToken } from './types';\nimport type { SelectionCapability } from './types';\n\nexport { selectionPlugin } from './selection.plugin';\nexport type {\n SelectionCapability,\n SelectionEndpoint,\n SelectionMenuAnchor,\n SelectionRangeInput,\n SelectionSnapshot,\n TextPosition,\n TextRange,\n} from './types';\nexport type { SelectionSegment } from './geometry';\n// Selection-handle policy (the touch affordance): pure geometry + the drag\n// session, consumed by the framework adapters' handle views.\nexport {\n HANDLE_BAR,\n HANDLE_HEAD,\n HANDLE_PAD,\n createSelectionHandleDrag,\n selectionHandleGeom,\n} from './handles';\nexport type {\n SelectionHandleDragSession,\n SelectionHandleEndpoint,\n SelectionHandleGeom,\n SelectionHandleTarget,\n SelectionHandleView,\n} from './handles';\n\n/**\n * The selection capability token, narrowed to the public\n * {@link SelectionCapability} lens. It is the SAME runtime token the plugin\n * provides — the host-only surface (pointer gestures, geometry warming) is\n * reachable only via `@embedpdf/plugin-selection/internal`.\n */\nexport const SelectionToken =\n SelectionHostToken as unknown as CapabilityToken<SelectionCapability>;\n"],"mappings":";;;;;;;;;;;;;;;AAkBA,MAAa,wBACX,aAAuE;CACrE,IAAI;CACJ,OAAOA;CACP,OAAO;CACP,UAAU,CAAC,gBAAgB;CAG3B,UAAU,CAAC,aAAa;CACxB,cAAc;CACd,QAAQ;CACR,YAAY;CACZ,OAAO,QAAQ;EACb,MAAM,cAAc,IAAI,IAAI,gBAAgB;EAC5C,MAAM,YAAY,IAAI,IAAIA,gBAAc;EACxC,MAAM,WAAW,IAAI,OAAO,aAAa;EACzC,YAAY,gBACV,wBAAwB,WAAW,aAAa,YAAY,KAAA,CAAS,CACvE;CACF;AACF,CAAC;;;;;;;;;;;;;;;;;;;;;;;ACiBH,MAAa,cAAc;AAC3B,MAAa,aAAa;AAC1B,MAAa,aAAa;;;AAI1B,MAAM,qBAAqB;AAe3B,MAAM,YAAY,GAAU,OAAqB;CAAE,IAAI,EAAE,IAAI,EAAE,KAAK;CAAG,IAAI,EAAE,IAAI,EAAE,KAAK;AAAE;;;;;AAM1F,SAAgB,oBACd,MACA,IACA,MAC4B;CAG5B,MAAM,UAAU,SAAS,UAAU,GAAG,UAAU,IAAI,GAAG,UAAU;CACjE,MAAM,CAAC,WAAW,aAAa,aAAa,GAAG,WAAW,UAAU,UAAU,KAAK;CACnF,MAAM,QAAQ,KAAK,UAAU,GAAG,KAAK,SAAS;CAC9C,MAAM,QAAQ,KAAK,UAAU,GAAG,KAAK,SAAS;CAC9C,IAAI,CAAC,SAAS,CAAC,OAAO,OAAO;CAC7B,MAAM,KAAK,MAAM,IAAI,MAAM;CAC3B,MAAM,KAAK,MAAM,IAAI,MAAM;CAC3B,MAAM,SAAS,KAAK,MAAM,IAAI,EAAE;CAChC,IAAI,WAAW,GAAG,OAAO;CAEzB,MAAM,WAAY,KAAK,MAAM,IAAI,EAAE,IAAI,MAAO,KAAK,KAAK;CACxD,MAAM,UAAU,KAAK,IAAI,QAAQ,IAAI;CACrC,MAAM,KAAK,KAAK;CAChB,MAAM,KAAK,KAAK;CAChB,MAAM,OACJ,SAAS,UACL;EAAE,GAAG,MAAM,IAAK,KAAA,KAAoB;EAAG,GAAG,MAAM,IAAK,KAAA,KAAoB;CAAE,IAC3E;EAAE,GAAG,MAAM,IAAK,KAAA,KAAoB;EAAG,GAAG,MAAM,IAAK,KAAA,KAAoB;CAAE;CACjF,OAAO;EAAE,KAAK;GAAE,MAAM;GAAO,IAAI;EAAM;EAAG;EAAQ;EAAU;EAAS;CAAK;AAC5E;;;;;;;;;;;AAmBA,SAAgB,0BACd,WACA,MACA,UACA,YAC4B;CAI5B,MAAM,IAAI,SAAS;CACnB,MAAM,cAAc,SAAS,SAAS,EAAE,YAAY,EAAE,QAAQ,GAAG,SAAS,EAAE,UAAU,EAAE,UAAU,CAAC;CACnG,IAAI,QAAQ;CACZ,IAAI,UAAU;CACd,OAAO;EACL,OAAO,YAAY;GACjB,IAAI,CAAC,OAAO;IACV,IAAI,CAAC,UAAU,QAAQ,SAAS,KAAK,WAAW,GAAG;IACnD,QAAQ;GACV;GACA,MAAM,MAAM,KAAK,OAAO,OAAO;GAC/B,IAAI,KAAK;IACP,UAAU,IAAI;IACd,UAAU,SAAS,IAAI,KAAK,IAAI,KAAK;GACvC,OAAO;IACL,MAAM,IAAI,KAAK,YAAY,SAAS,OAAO;IAC3C,IAAI,GAAG,UAAU,SAAS,SAAS,CAAC;GACtC;EACF;EACA,WAAW;GACT,IAAI,OAAO,UAAU,IAAI;EAC3B;CACF;AACF;;;;;;;;;AC5GA,MAAa,iBACXC"}
package/dist/internal.cjs CHANGED
@@ -1,5 +1,5 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_reducer = require("./reducer-Dyf8p5h-.cjs");
2
+ const require_reducer = require("./reducer-B6uL_O7s.cjs");
3
3
  exports.SelectionToken = require_reducer.SelectionToken;
4
4
  exports.buildSelectionPageGeometry = require_reducer.buildSelectionPageGeometry;
5
5
  exports.contentPointToPdf = require_reducer.contentPointToPdf;
@@ -1,6 +1,6 @@
1
- import { _ as contentPointToPdf, a as SelectionHostCapability, d as SelectionToken, g as buildSelectionPageGeometry, m as SelectionPageGeometry, n as SelectionAction, s as SelectionRange, t as GlyphPointer, u as SelectionState, v as toContentSegment, y as toContentTextQuad } from "./types-DtWaZIsm.cjs";
1
+ import { _ as contentPointToPdf, a as SelectionHostCapability, d as SelectionToken, g as buildSelectionPageGeometry, m as SelectionPageGeometry, n as SelectionAction, s as SelectionRange, t as GlyphPointer, u as SelectionState, v as toContentSegment, y as toContentTextQuad } from "./types-BCPuB3iZ.cjs";
2
2
  import { PluginContext } from "@embedpdf/core";
3
- import { InteractionCapability, InteractionHandler } from "@embedpdf/plugin-interaction";
3
+ import { InteractionCapability, InteractionHandler, PlatformFeedback } from "@embedpdf/plugin-interaction";
4
4
  //#region src/capability.d.ts
5
5
  /**
6
6
  * The selection capability — ONE closure returning the host lens (the
@@ -52,7 +52,7 @@ declare function createSelectionCapability(ctx: PluginContext<SelectionState, Se
52
52
  * - down off-text → deselect, and DON'T capture (so it can't block)
53
53
  * - hover → I-beam only when over text, else the pointer cursor
54
54
  */
55
- declare function createTextSelectHandler(selection: SelectionHostCapability, interaction: InteractionCapability): InteractionHandler;
55
+ declare function createTextSelectHandler(selection: SelectionHostCapability, interaction: InteractionCapability, feedback?: PlatformFeedback): InteractionHandler;
56
56
  //#endregion
57
57
  //#region src/reducer.d.ts
58
58
  declare const initialSelectionState: SelectionState;
@@ -1,6 +1,6 @@
1
- import { _ as contentPointToPdf, a as SelectionHostCapability, d as SelectionToken, g as buildSelectionPageGeometry, m as SelectionPageGeometry, n as SelectionAction, s as SelectionRange, t as GlyphPointer, u as SelectionState, v as toContentSegment, y as toContentTextQuad } from "./types-DtWaZIsm.js";
1
+ import { _ as contentPointToPdf, a as SelectionHostCapability, d as SelectionToken, g as buildSelectionPageGeometry, m as SelectionPageGeometry, n as SelectionAction, s as SelectionRange, t as GlyphPointer, u as SelectionState, v as toContentSegment, y as toContentTextQuad } from "./types-BCPuB3iZ.js";
2
2
  import { PluginContext } from "@embedpdf/core";
3
- import { InteractionCapability, InteractionHandler } from "@embedpdf/plugin-interaction";
3
+ import { InteractionCapability, InteractionHandler, PlatformFeedback } from "@embedpdf/plugin-interaction";
4
4
  //#region src/capability.d.ts
5
5
  /**
6
6
  * The selection capability — ONE closure returning the host lens (the
@@ -52,7 +52,7 @@ declare function createSelectionCapability(ctx: PluginContext<SelectionState, Se
52
52
  * - down off-text → deselect, and DON'T capture (so it can't block)
53
53
  * - hover → I-beam only when over text, else the pointer cursor
54
54
  */
55
- declare function createTextSelectHandler(selection: SelectionHostCapability, interaction: InteractionCapability): InteractionHandler;
55
+ declare function createTextSelectHandler(selection: SelectionHostCapability, interaction: InteractionCapability, feedback?: PlatformFeedback): InteractionHandler;
56
56
  //#endregion
57
57
  //#region src/reducer.d.ts
58
58
  declare const initialSelectionState: SelectionState;
package/dist/internal.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as buildSelectionPageGeometry, c as toContentTextQuad, i as createSelectionCapability, l as SelectionToken, n as selectionReducer, o as contentPointToPdf, r as createTextSelectHandler, s as toContentSegment, t as initialSelectionState } from "./reducer-CZG2TZgo.js";
1
+ import { a as buildSelectionPageGeometry, c as toContentTextQuad, i as createSelectionCapability, l as SelectionToken, n as selectionReducer, o as contentPointToPdf, r as createTextSelectHandler, s as toContentSegment, t as initialSelectionState } from "./reducer-BhLvJwVe.js";
2
2
  export { SelectionToken, buildSelectionPageGeometry, contentPointToPdf, createSelectionCapability, createTextSelectHandler, initialSelectionState, selectionReducer, toContentSegment, toContentTextQuad };