Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion examples/vue-vuetify/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ declare module 'vue' {
Drawer: typeof import('./src/components/drawer-system/Drawer.vue')['default']
DrawerProvider: typeof import('./src/components/drawer-system/DrawerProvider.vue')['default']
DrawerToggleButton: typeof import('./src/components/drawer-system/DrawerToggleButton.vue')['default']
HelloWorld: typeof import('./src/components/HelloWorld.vue')['default']
PageControls: typeof import('./src/components/PageControls.vue')['default']
PrintDialog: typeof import('./src/components/PrintDialog.vue')['default']
Search: typeof import('./src/components/Search.vue')['default']
Expand Down
5 changes: 3 additions & 2 deletions packages/plugin-selection/src/lib/actions.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Action } from '@embedpdf/core';
import { PdfPageGeometry, Rect } from '@embedpdf/models';
import { SelectionRangeX } from './types';
import { GlyphAccelerationModel } from './utils';

export const CACHE_PAGE_GEOMETRY = 'CACHE_PAGE_GEOMETRY';
export const SET_SELECTION = 'SET_SELECTION';
Expand All @@ -13,7 +14,7 @@ export const RESET = 'RESET';

export interface CachePageGeometryAction extends Action {
type: typeof CACHE_PAGE_GEOMETRY;
payload: { page: number; geo: PdfPageGeometry };
payload: { page: number; geo: GlyphAccelerationModel };
}
export interface SetSelectionAction extends Action {
type: typeof SET_SELECTION;
Expand Down Expand Up @@ -56,7 +57,7 @@ export type SelectionAction =
| SetSlicesAction
| ResetAction;

export const cachePageGeometry = (page: number, geo: PdfPageGeometry): CachePageGeometryAction => ({
export const cachePageGeometry = (page: number, geo: GlyphAccelerationModel): CachePageGeometryAction => ({
type: CACHE_PAGE_GEOMETRY,
payload: { page, geo },
});
Expand Down
111 changes: 80 additions & 31 deletions packages/plugin-selection/src/lib/selection-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ import {
RegisterSelectionOnPageOptions,
SelectionRectsCallback,
} from './types';
import { sliceBounds, rectsWithinSlice, glyphAt } from './utils';
import {
sliceBounds,
rectsWithinSlice,
glyphAt,
findNearestGlyphWithModel,
GlyphAccelerationModel,
buildGlyphAccelerationModel,
} from './utils';

export class SelectionPlugin extends BasePlugin<
SelectionPluginConfig,
Expand All @@ -59,6 +66,7 @@ export class SelectionPlugin extends BasePlugin<
/* interactive state */
private selecting = false;
private anchor?: { page: number; index: number };
private mouseDown = false;

/** Page callbacks for rect updates */
private pageCallbacks = new Map<number, (data: SelectionRectsCallback) => void>();
Expand Down Expand Up @@ -145,50 +153,75 @@ export class SelectionPlugin extends BasePlugin<
rects: selector.selectRectsForPage(this.state, pageIndex),
boundingRect: selector.selectBoundingRectForPage(this.state, pageIndex),
});
let mouseDownData: {
x: number;
y: number;
num: number;
time: number;
clientX: number;
clientY: number;
} | null = null;

const handlers: PointerEventHandlersWithLifecycle<PointerEvent> = {
onPointerDown: (point: Position, _evt, modeId) => {
onPointerDown: (point: Position, evt, modeId) => {
if (!this.enabledModes.has(modeId)) return;

// Clear the selection
this.clearSelection();

// Get geometry from cache (or load if needed)
const cached = this.state.geometry[pageIndex];
if (cached) {
const g = glyphAt(cached, point);
if (g !== -1) {
this.beginSelection(pageIndex, g);
}
}
if (!cached) return;

const res = findNearestGlyphWithModel(cached, { x: point.x, y: point.y });
const num = res?.globalIndex ?? -1;

mouseDownData = {
x: point.x,
y: point.y,
num,
time: performance.now(),
clientX: evt.clientX,
clientY: evt.clientY,
};
this.mouseDown = true;
},
onPointerMove: (point: Position, _evt, modeId) => {
onPointerMove: (point: Position, evt, modeId) => {
if (!this.enabledModes.has(modeId)) return;

// Get cached geometry (should be instant if already loaded)
const cached = this.state.geometry[pageIndex];
if (cached) {
const g = glyphAt(cached, point);

// Update cursor
if (g !== -1) {
this.interactionManagerCapability?.setCursor('selection-text', 'text', 10);
} else {
this.interactionManagerCapability?.removeCursor('selection-text');
}

// Update selection if we're selecting
if (this.selecting && g !== -1) {
this.updateSelection(pageIndex, g);
}
if (!cached) return;
const res = findNearestGlyphWithModel(cached, point);
if (!res) {
this.interactionManagerCapability?.removeCursor('selection-text');
return;
}
if (res.isExactMatch) {
this.interactionManagerCapability?.setCursor('selection-text', 'text', 10);
} else {
this.interactionManagerCapability?.removeCursor('selection-text');
}

const g = res.globalIndex;
const isMouseDown = this.mouseDown;
if (isMouseDown && mouseDownData && !this.selecting) {
const deltaX = evt.clientX - mouseDownData.clientX;
const deltaY = evt.clientY - mouseDownData.clientY;
const distance = deltaX * deltaX + deltaY * deltaY;
if (distance > 25) this.beginSelection(pageIndex, g);
} else if (this.selecting) {
this.updateSelection(pageIndex, g);
}
},
onPointerUp: (_point: Position, _evt, modeId) => {
if (!this.enabledModes.has(modeId)) return;
this.mouseDown = false;
mouseDownData = null;
this.endSelection();
},
onHandlerActiveEnd: (modeId) => {
if (!this.enabledModes.has(modeId)) return;
this.mouseDown = false;
mouseDownData = null;
this.clearSelection();
},
};
Expand Down Expand Up @@ -228,19 +261,34 @@ export class SelectionPlugin extends BasePlugin<
});
}

private getNewPageGeometryAndCache(pageIdx: number): PdfTask<PdfPageGeometry> {
private getNewPageGeometryAndCache(pageIdx: number): PdfTask<GlyphAccelerationModel> {
if (!this.coreState.core.document)
return PdfTaskHelper.reject({ code: PdfErrorCode.NotFound, message: 'Doc Not Found' });
const page = this.coreState.core.document.pages.find((p) => p.index === pageIdx)!;
const resTask: PdfTask<GlyphAccelerationModel> = new Task();

const task = this.engine.getPageGeometry(this.coreState.core.document, page);
task.wait((geo) => {
this.dispatch(cachePageGeometry(pageIdx, geo));
const model = buildGlyphAccelerationModel(geo);
this.dispatch(cachePageGeometry(pageIdx, model));
resTask.resolve(model);
}, ignore);
return task;

// listen task abort to abort webworker task
resTask.wait(
() => {},
(err) => {
if (err.type === 'abort') {
task.abort(err.reason);
}
},
);

return resTask;
}

/* ── geometry cache ───────────────────────────────────── */
private getOrLoadGeometry(pageIdx: number): PdfTask<PdfPageGeometry> {
private getOrLoadGeometry(pageIdx: number): PdfTask<GlyphAccelerationModel> {
const cached = this.state.geometry[pageIdx];
if (cached) return PdfTaskHelper.resolve(cached);

Expand All @@ -264,6 +312,7 @@ export class SelectionPlugin extends BasePlugin<

private clearSelection() {
this.selecting = false;
this.mouseDown = false;
this.anchor = undefined;
this.dispatch(clearSelection());
this.selChange$.emit(null);
Expand Down Expand Up @@ -296,10 +345,10 @@ export class SelectionPlugin extends BasePlugin<

for (let p = range.start.page; p <= range.end.page; p++) {
const geo = this.state.geometry[p];
const sb = sliceBounds(range, geo, p);
const sb = sliceBounds(range, geo.geo, p);
if (!sb) continue;

allRects[p] = rectsWithinSlice(geo!, sb.from, sb.to);
allRects[p] = rectsWithinSlice(geo.geo!, sb.from, sb.to);
allSlices[p] = { start: sb.from, count: sb.to - sb.from + 1 };
}

Expand Down
3 changes: 2 additions & 1 deletion packages/plugin-selection/src/lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { BasePluginConfig, EventHook } from '@embedpdf/core';
import { PdfPageGeometry, PdfTask, Rect } from '@embedpdf/models';
import { GlyphAccelerationModel } from './utils';

export interface SelectionPluginConfig extends BasePluginConfig {}

Expand All @@ -16,7 +17,7 @@ export interface SelectionRangeX {

export interface SelectionState {
/** page → geometry cache */
geometry: Record<number, PdfPageGeometry>;
geometry: Record<number, GlyphAccelerationModel>;
/** current selection or null */
rects: Record<number, Rect[]>;
selection: SelectionRangeX | null;
Expand Down
Loading