kviewer 0.0.8 → 0.0.9

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/module.d.mts CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as _nuxt_schema from '@nuxt/schema';
2
- export { ExportPdfOptions } from '../dist/runtime/public-types.js';
2
+ export { ExportPdfOptions, SignatureData, SignatureHandlers, ViewMode } from '../dist/runtime/public-types.js';
3
3
 
4
4
  interface ModuleOptions {
5
5
  /**
package/dist/module.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "kviewer",
3
3
  "configKey": "kviewer",
4
- "version": "0.0.8",
4
+ "version": "0.0.9",
5
5
  "builder": {
6
6
  "@nuxt/module-builder": "1.0.2",
7
7
  "unbuild": "3.6.1"
@@ -10,6 +10,8 @@ export interface IEditorOptions {
10
10
  onChange: (id: string, updates: Partial<IAnnotationStore>) => void;
11
11
  /** When provided and returns true, only pen/mouse input can draw. */
12
12
  getStylusModeEnabled?: () => boolean;
13
+ /** Delay in ms before consecutive freehand strokes are finalized as a single annotation. */
14
+ freehandGroupingDelay?: number;
13
15
  }
14
16
  export interface IShapeGroup {
15
17
  id: string;
@@ -30,8 +32,9 @@ export declare abstract class Editor {
30
32
  shapeGroupStore: Map<string, IShapeGroup>;
31
33
  currentShapeGroup: IShapeGroup | null;
32
34
  protected getStylusModeEnabled?: () => boolean;
35
+ protected freehandGroupingDelay: number;
33
36
  static MinSize: number;
34
- constructor({ userName, konvaStage, pageNumber, annotation, onAdd, editorType, onChange, getStylusModeEnabled, }: IEditorOptions & {
37
+ constructor({ userName, konvaStage, pageNumber, annotation, onAdd, editorType, onChange, getStylusModeEnabled, freehandGroupingDelay, }: IEditorOptions & {
35
38
  editorType: AnnotationType;
36
39
  });
37
40
  private dispatchAddEvent;
@@ -65,5 +68,5 @@ export declare abstract class Editor {
65
68
  [pageNumber: number]: number;
66
69
  };
67
70
  static TimerClear(pageNumber: number): void;
68
- static TimerStart(pageNumber: number, callback: (pageNumber: number) => void): void;
71
+ static TimerStart(pageNumber: number, callback: (pageNumber: number) => void, delay?: number): void;
69
72
  }
@@ -14,6 +14,7 @@ export class Editor {
14
14
  shapeGroupStore = /* @__PURE__ */ new Map();
15
15
  currentShapeGroup;
16
16
  getStylusModeEnabled;
17
+ freehandGroupingDelay;
17
18
  static MinSize = 8;
18
19
  constructor({
19
20
  userName,
@@ -23,7 +24,8 @@ export class Editor {
23
24
  onAdd,
24
25
  editorType,
25
26
  onChange,
26
- getStylusModeEnabled
27
+ getStylusModeEnabled,
28
+ freehandGroupingDelay
27
29
  }) {
28
30
  this.userName = userName;
29
31
  this.id = `${pageNumber}_${editorType}`;
@@ -36,6 +38,7 @@ export class Editor {
36
38
  this.onChange = onChange || (() => {
37
39
  });
38
40
  this.getStylusModeEnabled = getStylusModeEnabled;
41
+ this.freehandGroupingDelay = freehandGroupingDelay ?? 1e3;
39
42
  this.disableEditMode();
40
43
  this.enableEditMode();
41
44
  }
@@ -235,11 +238,11 @@ export class Editor {
235
238
  window.clearTimeout(timer);
236
239
  }
237
240
  }
238
- static TimerStart(pageNumber, callback) {
241
+ static TimerStart(pageNumber, callback, delay = 1e3) {
239
242
  Editor.Timer[pageNumber] = window.setTimeout(() => {
240
243
  if (typeof callback === "function") {
241
244
  callback(pageNumber);
242
245
  }
243
- }, 1e3);
246
+ }, delay);
244
247
  }
245
248
  }
@@ -66,6 +66,17 @@ export declare class Selector {
66
66
  activateMarquee(pageNumber: number): void;
67
67
  private bindMarqueeEvents;
68
68
  private rectsIntersect;
69
+ /**
70
+ * Check whether a line segment intersects an axis-aligned rectangle
71
+ * (expanded by `tolerance` on each side to match the visual eraser width).
72
+ * Uses Liang-Barsky line clipping.
73
+ */
74
+ private segmentIntersectsRect;
75
+ /**
76
+ * Check whether any segment of an eraser path intersects a rectangle.
77
+ * Points is a flat array [x0, y0, x1, y1, ...] in page-space coordinates.
78
+ */
79
+ private eraserPathIntersectsRect;
69
80
  activateEraser(pageNumber: number): void;
70
81
  private eraserLine;
71
82
  private eraserStage;
@@ -526,6 +526,60 @@ export class Selector {
526
526
  rectsIntersect(a, b) {
527
527
  return a.x < b.x + b.width && a.x + a.width > b.x && a.y < b.y + b.height && a.y + a.height > b.y;
528
528
  }
529
+ /**
530
+ * Check whether a line segment intersects an axis-aligned rectangle
531
+ * (expanded by `tolerance` on each side to match the visual eraser width).
532
+ * Uses Liang-Barsky line clipping.
533
+ */
534
+ segmentIntersectsRect(x1, y1, x2, y2, rect, tolerance) {
535
+ const left = rect.x - tolerance;
536
+ const top = rect.y - tolerance;
537
+ const right = rect.x + rect.width + tolerance;
538
+ const bottom = rect.y + rect.height + tolerance;
539
+ let tMin = 0;
540
+ let tMax = 1;
541
+ const dx = x2 - x1;
542
+ const dy = y2 - y1;
543
+ const edges = [
544
+ { p: -dx, q: x1 - left },
545
+ { p: dx, q: right - x1 },
546
+ { p: -dy, q: y1 - top },
547
+ { p: dy, q: bottom - y1 }
548
+ ];
549
+ for (const { p, q } of edges) {
550
+ if (p === 0) {
551
+ if (q < 0) return false;
552
+ } else {
553
+ const t = q / p;
554
+ if (p < 0) {
555
+ tMin = Math.max(tMin, t);
556
+ } else {
557
+ tMax = Math.min(tMax, t);
558
+ }
559
+ if (tMin > tMax) return false;
560
+ }
561
+ }
562
+ return true;
563
+ }
564
+ /**
565
+ * Check whether any segment of an eraser path intersects a rectangle.
566
+ * Points is a flat array [x0, y0, x1, y1, ...] in page-space coordinates.
567
+ */
568
+ eraserPathIntersectsRect(points, rect, tolerance) {
569
+ for (let i = 0; i + 3 < points.length; i += 2) {
570
+ if (this.segmentIntersectsRect(
571
+ points[i],
572
+ points[i + 1],
573
+ points[i + 2],
574
+ points[i + 3],
575
+ rect,
576
+ tolerance
577
+ )) {
578
+ return true;
579
+ }
580
+ }
581
+ return false;
582
+ }
529
583
  activateEraser(pageNumber) {
530
584
  const konvaCanvas = this.konvaCanvasStore.get(pageNumber);
531
585
  if (!konvaCanvas) return;
@@ -569,17 +623,20 @@ export class Selector {
569
623
  konvaStage.on("mouseup touchend", (e) => {
570
624
  if (!this.eraserLine || !this.eraserStage) return;
571
625
  if (this.isStylusRejected(e.evt)) return;
572
- const eraserRect = this.eraserLine.getClientRect();
626
+ const points = this.eraserLine.points();
573
627
  this.eraserLine.destroy();
574
628
  this.eraserLine = null;
575
- if (eraserRect.width < 3 && eraserRect.height < 3) {
629
+ if (points.length < 4) {
576
630
  this.eraserStage = null;
577
631
  return;
578
632
  }
633
+ const scale = this.eraserStage.scaleX() ?? 1;
634
+ const screenPoints = points.map((v) => v * scale);
635
+ const tolerance = 6;
579
636
  const groups = this.getPageShapeGroups(this.eraserStage);
580
637
  const toDelete = [];
581
638
  groups.forEach((group) => {
582
- if (this.rectsIntersect(eraserRect, group.getClientRect())) {
639
+ if (this.eraserPathIntersectsRect(screenPoints, group.getClientRect(), tolerance)) {
583
640
  toDelete.push(group.id());
584
641
  }
585
642
  });
@@ -32,11 +32,13 @@ export declare class Painter {
32
32
  private callbacks;
33
33
  private getStylusModeEnabled?;
34
34
  private getReadonly?;
35
- constructor({ userName, callbacks, getStylusModeEnabled, getReadonly, }: {
35
+ private freehandGroupingDelay;
36
+ constructor({ userName, callbacks, getStylusModeEnabled, getReadonly, freehandGroupingDelay, }: {
36
37
  userName: string;
37
38
  callbacks: PainterCallbacks;
38
39
  getStylusModeEnabled?: () => boolean;
39
40
  getReadonly?: () => boolean;
41
+ freehandGroupingDelay?: number;
40
42
  });
41
43
  private editFreeText;
42
44
  private bindGlobalEvents;
@@ -34,16 +34,19 @@ export class Painter {
34
34
  callbacks;
35
35
  getStylusModeEnabled;
36
36
  getReadonly;
37
+ freehandGroupingDelay;
37
38
  constructor({
38
39
  userName,
39
40
  callbacks,
40
41
  getStylusModeEnabled,
41
- getReadonly
42
+ getReadonly,
43
+ freehandGroupingDelay
42
44
  }) {
43
45
  this.userName = userName;
44
46
  this.callbacks = callbacks;
45
47
  this.getStylusModeEnabled = getStylusModeEnabled;
46
48
  this.getReadonly = getReadonly;
49
+ this.freehandGroupingDelay = freehandGroupingDelay ?? 1e3;
47
50
  this.store = new Store();
48
51
  this.selector = new Selector({
49
52
  konvaCanvasStore: this.konvaCanvasStore,
@@ -244,7 +247,8 @@ export class Painter {
244
247
  onChange: (id, updates) => {
245
248
  this.updateStore(id, updates);
246
249
  },
247
- getStylusModeEnabled: this.getStylusModeEnabled
250
+ getStylusModeEnabled: this.getStylusModeEnabled,
251
+ freehandGroupingDelay: this.freehandGroupingDelay
248
252
  };
249
253
  let editor = null;
250
254
  switch (annotation.type) {
@@ -86,7 +86,7 @@ export class EditorFreeHand extends Editor {
86
86
  }
87
87
  });
88
88
  this.currentShapeGroup = null;
89
- });
89
+ }, this.freehandGroupingDelay);
90
90
  return;
91
91
  }
92
92
  Editor.TimerStart(this.pageNumber, () => {
@@ -98,7 +98,7 @@ export class EditorFreeHand extends Editor {
98
98
  }
99
99
  });
100
100
  this.currentShapeGroup = null;
101
- });
101
+ }, this.freehandGroupingDelay);
102
102
  this.line = null;
103
103
  }
104
104
  /**
@@ -15,6 +15,8 @@ type __VLS_Props = {
15
15
  shapeDetection?: boolean;
16
16
  /** When false, global keyboard shortcuts (e.g. Cmd+F) are suppressed. Used by ViewerTabs to prevent hidden viewers from capturing input. */
17
17
  active?: boolean;
18
+ /** Delay in ms before consecutive freehand strokes are finalized as a single annotation. Set to 0 to disable grouping. Default: 1000. */
19
+ freehandGroupingDelay?: number;
18
20
  };
19
21
  declare function exportPdf(options?: ExportPdfOptions): Promise<Uint8Array>;
20
22
  type ImportMode = 'replace' | 'merge';
@@ -40,6 +42,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
40
42
  setFormFieldValue: (fieldName: string, value: string | boolean | string[]) => void;
41
43
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
42
44
  userName: string;
45
+ freehandGroupingDelay: number;
43
46
  zoom: number;
44
47
  readonly: boolean;
45
48
  active: boolean;
@@ -185,7 +185,8 @@ const props = defineProps({
185
185
  zoom: { type: Number, required: false, default: 1 },
186
186
  readonly: { type: Boolean, required: false, default: false },
187
187
  shapeDetection: { type: Boolean, required: false, default: false },
188
- active: { type: Boolean, required: false, default: true }
188
+ active: { type: Boolean, required: false, default: true },
189
+ freehandGroupingDelay: { type: Number, required: false, default: 1e3 }
189
190
  });
190
191
  const viewerRoot = ref(null);
191
192
  const scrollContainer = ref(null);
@@ -609,7 +610,8 @@ function onGlobalKeydown(event) {
609
610
  onMounted(() => {
610
611
  painter = createAnnotationEngine(viewerState, {
611
612
  userName: props.userName,
612
- onRequestTextInput
613
+ onRequestTextInput,
614
+ freehandGroupingDelay: props.freehandGroupingDelay
613
615
  });
614
616
  viewerState.selectTool(viewerState.activeTool.value);
615
617
  if (props.stamps) viewerState.stamps.value = props.stamps;
@@ -674,6 +676,15 @@ watch(
674
676
  viewerState.stamps.value = newStamps ?? [];
675
677
  }
676
678
  );
679
+ watch(
680
+ () => props.signatureHandlers,
681
+ (newHandlers) => {
682
+ viewerState.signatureHandlers = newHandlers ?? null;
683
+ if (newHandlers) {
684
+ viewerState.loadSignatures();
685
+ }
686
+ }
687
+ );
677
688
  onBeforeUnmount(() => {
678
689
  window.removeEventListener("keydown", onGlobalKeydown);
679
690
  scrollContainer.value?.removeEventListener("wheel", onSinglePageWheel);
@@ -15,6 +15,8 @@ type __VLS_Props = {
15
15
  shapeDetection?: boolean;
16
16
  /** When false, global keyboard shortcuts (e.g. Cmd+F) are suppressed. Used by ViewerTabs to prevent hidden viewers from capturing input. */
17
17
  active?: boolean;
18
+ /** Delay in ms before consecutive freehand strokes are finalized as a single annotation. Set to 0 to disable grouping. Default: 1000. */
19
+ freehandGroupingDelay?: number;
18
20
  };
19
21
  declare function exportPdf(options?: ExportPdfOptions): Promise<Uint8Array>;
20
22
  type ImportMode = 'replace' | 'merge';
@@ -40,6 +42,7 @@ declare const __VLS_base: import("vue").DefineComponent<__VLS_Props, {
40
42
  setFormFieldValue: (fieldName: string, value: string | boolean | string[]) => void;
41
43
  }, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
42
44
  userName: string;
45
+ freehandGroupingDelay: number;
43
46
  zoom: number;
44
47
  readonly: boolean;
45
48
  active: boolean;
@@ -14,17 +14,27 @@
14
14
  Click to sign
15
15
  </span>
16
16
  </div>
17
+
18
+ <ClientOnly>
19
+ <SignatureDrawModal
20
+ v-model:open="drawModalOpen"
21
+ @submit="onDrawSubmit"
22
+ @cancel="drawModalOpen = false"
23
+ />
24
+ </ClientOnly>
17
25
  </template>
18
26
 
19
27
  <script setup>
20
- import { computed } from "vue";
28
+ import { ref, computed } from "vue";
21
29
  import { useFormFields } from "../../composables/useFormFields";
22
30
  import { useViewerState } from "../../composables/useViewerState";
31
+ import SignatureDrawModal from "../modals/SignatureDrawModal.vue";
23
32
  const props = defineProps({
24
33
  field: { type: Object, required: true }
25
34
  });
26
35
  const formFields = useFormFields();
27
36
  const state = useViewerState();
37
+ const drawModalOpen = ref(false);
28
38
  const signatureUrl = computed(() => {
29
39
  const fv = formFields.getFieldValue(props.field.id);
30
40
  if (!fv || typeof fv.value !== "string" || fv.value === "") return null;
@@ -50,5 +60,17 @@ async function onClickSign() {
50
60
  }
51
61
  }
52
62
  }
63
+ drawModalOpen.value = true;
64
+ }
65
+ async function onDrawSubmit(imageUrl) {
66
+ drawModalOpen.value = false;
67
+ if (state.signatureHandlers) {
68
+ const saved = await state.saveSignature(imageUrl);
69
+ if (saved) {
70
+ formFields.setFieldValue(props.field.id, saved.imageUrl);
71
+ return;
72
+ }
73
+ }
74
+ formFields.setFieldValue(props.field.id, imageUrl);
53
75
  }
54
76
  </script>
@@ -18,7 +18,7 @@
18
18
  @click="state.selectTool(AnnotationType.FREETEXT)"
19
19
  />
20
20
  <StampPicker />
21
- <SignaturePicker />
21
+ <SignaturePicker v-if="state.signatureHandlers" />
22
22
  <ToolButton
23
23
  :tool="getDef(AnnotationType.RECTANGLE)"
24
24
  :active="state.activeTool.value === AnnotationType.RECTANGLE"
@@ -4,4 +4,5 @@ export declare function createAnnotationEngine(viewerState: ViewerState, options
4
4
  userName?: string;
5
5
  onRequestTextInput: PainterCallbacks['onRequestTextInput'];
6
6
  onRequestDeleteConfirm?: PainterCallbacks['onRequestDeleteConfirm'];
7
+ freehandGroupingDelay?: number;
7
8
  }): Painter;
@@ -64,7 +64,8 @@ export function createAnnotationEngine(viewerState, options) {
64
64
  userName: options.userName ?? "User",
65
65
  callbacks,
66
66
  getStylusModeEnabled: () => viewerState.stylusMode.value,
67
- getReadonly: () => viewerState.readonly.value
67
+ getReadonly: () => viewerState.readonly.value,
68
+ freehandGroupingDelay: options.freehandGroupingDelay
68
69
  });
69
70
  viewerState.painter.value = painter;
70
71
  return painter;
@@ -1,2 +1,4 @@
1
1
  export type { ExportPdfOptions } from './annotation/pdf-export/export.js';
2
2
  export type { ViewerTabItem, AddTabOptions } from './components/ViewerTabs.vue.js';
3
+ export type { SignatureData, SignatureHandlers } from './annotation/engine/types.js';
4
+ export type { ViewMode } from './composables/viewMode.js';
package/dist/types.d.mts CHANGED
@@ -1,4 +1,4 @@
1
- export { type ExportPdfOptions } from '../dist/runtime/public-types.js'
1
+ export { type ExportPdfOptions, type SignatureData, type SignatureHandlers, type ViewMode } from '../dist/runtime/public-types.js'
2
2
 
3
3
  export { default } from './module.mjs'
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kviewer",
3
- "version": "0.0.8",
3
+ "version": "0.0.9",
4
4
  "description": "Kabema PDF Editor",
5
5
  "repository": "kabema/kviewer",
6
6
  "license": "MIT",