js-draw 1.16.1 → 1.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (99) hide show
  1. package/dist/bundle.js +2 -2
  2. package/dist/bundledStyles.js +1 -1
  3. package/dist/cjs/Editor.d.ts +76 -6
  4. package/dist/cjs/Editor.js +89 -89
  5. package/dist/cjs/Pointer.d.ts +2 -1
  6. package/dist/cjs/Pointer.js +9 -2
  7. package/dist/cjs/commands/localization.d.ts +1 -0
  8. package/dist/cjs/commands/localization.js +1 -0
  9. package/dist/cjs/commands/uniteCommands.d.ts +5 -1
  10. package/dist/cjs/commands/uniteCommands.js +33 -7
  11. package/dist/cjs/components/TextComponent.d.ts +36 -1
  12. package/dist/cjs/components/TextComponent.js +39 -1
  13. package/dist/cjs/components/builders/ArrowBuilder.js +1 -1
  14. package/dist/cjs/components/builders/PolylineBuilder.d.ts +35 -0
  15. package/dist/cjs/components/builders/PolylineBuilder.js +115 -0
  16. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +1 -1
  17. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +1 -1
  18. package/dist/cjs/components/lib.d.ts +1 -0
  19. package/dist/cjs/components/lib.js +3 -1
  20. package/dist/cjs/components/util/StrokeSmoother.js +4 -4
  21. package/dist/cjs/image/EditorImage.d.ts +4 -1
  22. package/dist/cjs/image/EditorImage.js +4 -1
  23. package/dist/cjs/inputEvents.d.ts +11 -1
  24. package/dist/cjs/localizations/comments.d.ts +3 -0
  25. package/dist/cjs/localizations/comments.js +3 -0
  26. package/dist/cjs/localizations/de.js +0 -2
  27. package/dist/cjs/localizations/es.js +2 -2
  28. package/dist/cjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
  29. package/dist/cjs/rendering/renderers/CanvasRenderer.js +16 -0
  30. package/dist/cjs/rendering/renderers/SVGRenderer.js +1 -1
  31. package/dist/cjs/toolbar/IconProvider.d.ts +6 -3
  32. package/dist/cjs/toolbar/IconProvider.js +6 -4
  33. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +24 -1
  34. package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
  35. package/dist/cjs/toolbar/widgets/PenToolWidget.js +7 -1
  36. package/dist/cjs/tools/Eraser.js +1 -1
  37. package/dist/cjs/tools/InputFilter/InputStabilizer.js +3 -3
  38. package/dist/cjs/tools/PasteHandler.js +36 -10
  39. package/dist/cjs/tools/Pen.js +2 -2
  40. package/dist/cjs/tools/SelectionTool/SelectionTool.js +23 -4
  41. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +1 -1
  42. package/dist/cjs/tools/ToolController.d.ts +17 -1
  43. package/dist/cjs/tools/ToolController.js +21 -8
  44. package/dist/cjs/tools/localization.d.ts +2 -2
  45. package/dist/cjs/tools/localization.js +2 -2
  46. package/dist/cjs/util/ClipboardHandler.d.ts +27 -0
  47. package/dist/cjs/util/ClipboardHandler.js +205 -0
  48. package/dist/cjs/util/ClipboardHandler.test.d.ts +1 -0
  49. package/dist/cjs/version.d.ts +5 -0
  50. package/dist/cjs/version.js +6 -1
  51. package/dist/mjs/Editor.d.ts +76 -6
  52. package/dist/mjs/Editor.mjs +89 -89
  53. package/dist/mjs/Pointer.d.ts +2 -1
  54. package/dist/mjs/Pointer.mjs +9 -2
  55. package/dist/mjs/commands/localization.d.ts +1 -0
  56. package/dist/mjs/commands/localization.mjs +1 -0
  57. package/dist/mjs/commands/uniteCommands.d.ts +5 -1
  58. package/dist/mjs/commands/uniteCommands.mjs +33 -7
  59. package/dist/mjs/components/TextComponent.d.ts +36 -1
  60. package/dist/mjs/components/TextComponent.mjs +40 -2
  61. package/dist/mjs/components/builders/ArrowBuilder.mjs +1 -1
  62. package/dist/mjs/components/builders/PolylineBuilder.d.ts +35 -0
  63. package/dist/mjs/components/builders/PolylineBuilder.mjs +108 -0
  64. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +1 -1
  65. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +1 -1
  66. package/dist/mjs/components/lib.d.ts +1 -0
  67. package/dist/mjs/components/lib.mjs +1 -0
  68. package/dist/mjs/components/util/StrokeSmoother.mjs +4 -4
  69. package/dist/mjs/image/EditorImage.d.ts +4 -1
  70. package/dist/mjs/image/EditorImage.mjs +4 -1
  71. package/dist/mjs/inputEvents.d.ts +11 -1
  72. package/dist/mjs/localizations/comments.d.ts +3 -0
  73. package/dist/mjs/localizations/comments.mjs +3 -0
  74. package/dist/mjs/localizations/de.mjs +0 -2
  75. package/dist/mjs/localizations/es.mjs +2 -2
  76. package/dist/mjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
  77. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +16 -0
  78. package/dist/mjs/rendering/renderers/SVGRenderer.mjs +1 -1
  79. package/dist/mjs/toolbar/IconProvider.d.ts +6 -3
  80. package/dist/mjs/toolbar/IconProvider.mjs +6 -4
  81. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +24 -1
  82. package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
  83. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +7 -1
  84. package/dist/mjs/tools/Eraser.mjs +1 -1
  85. package/dist/mjs/tools/InputFilter/InputStabilizer.mjs +3 -3
  86. package/dist/mjs/tools/PasteHandler.mjs +36 -10
  87. package/dist/mjs/tools/Pen.mjs +2 -2
  88. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +23 -4
  89. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +1 -1
  90. package/dist/mjs/tools/ToolController.d.ts +17 -1
  91. package/dist/mjs/tools/ToolController.mjs +21 -8
  92. package/dist/mjs/tools/localization.d.ts +2 -2
  93. package/dist/mjs/tools/localization.mjs +2 -2
  94. package/dist/mjs/util/ClipboardHandler.d.ts +27 -0
  95. package/dist/mjs/util/ClipboardHandler.mjs +200 -0
  96. package/dist/mjs/util/ClipboardHandler.test.d.ts +1 -0
  97. package/dist/mjs/version.d.ts +5 -0
  98. package/dist/mjs/version.mjs +6 -1
  99. package/package.json +6 -6
@@ -0,0 +1,108 @@
1
+ import { Rect2, Color4, PathCommandType } from '@js-draw/math';
2
+ import Stroke from '../Stroke.mjs';
3
+ import Viewport from '../../Viewport.mjs';
4
+ import makeShapeFitAutocorrect from './autocorrect/makeShapeFitAutocorrect.mjs';
5
+ /**
6
+ * Creates strokes from line segments rather than Bézier curves.
7
+ *
8
+ * @beta Output behavior may change significantly between versions. For now, intended for debugging.
9
+ */
10
+ export const makePolylineBuilder = makeShapeFitAutocorrect((initialPoint, viewport) => {
11
+ const minFit = viewport.getSizeOfPixelOnCanvas();
12
+ return new PolylineBuilder(initialPoint, minFit, viewport);
13
+ });
14
+ export default class PolylineBuilder {
15
+ constructor(startPoint, minFitAllowed, viewport) {
16
+ this.minFitAllowed = minFitAllowed;
17
+ this.viewport = viewport;
18
+ this.parts = [];
19
+ this.widthAverageNumSamples = 1;
20
+ this.averageWidth = startPoint.width;
21
+ this.startPoint = {
22
+ ...startPoint,
23
+ pos: this.roundPoint(startPoint.pos),
24
+ };
25
+ this.lastPoint = this.startPoint.pos;
26
+ this.bbox = new Rect2(this.startPoint.pos.x, this.startPoint.pos.y, 0, 0);
27
+ this.parts = [
28
+ {
29
+ kind: PathCommandType.MoveTo,
30
+ point: this.startPoint.pos,
31
+ },
32
+ ];
33
+ }
34
+ getBBox() {
35
+ return this.bbox.grownBy(this.averageWidth);
36
+ }
37
+ getRenderingStyle() {
38
+ return {
39
+ fill: Color4.transparent,
40
+ stroke: {
41
+ color: this.startPoint.color,
42
+ width: this.roundDistance(this.averageWidth),
43
+ }
44
+ };
45
+ }
46
+ previewCurrentPath() {
47
+ const startPoint = this.startPoint.pos;
48
+ const commands = [...this.parts];
49
+ // TODO: For now, this is necesary for the path to be visible.
50
+ if (commands.length <= 1) {
51
+ commands.push({
52
+ kind: PathCommandType.LineTo,
53
+ point: startPoint,
54
+ });
55
+ }
56
+ return {
57
+ startPoint,
58
+ commands,
59
+ style: this.getRenderingStyle(),
60
+ };
61
+ }
62
+ previewFullPath() {
63
+ return [this.previewCurrentPath()];
64
+ }
65
+ preview(renderer) {
66
+ const paths = this.previewFullPath();
67
+ if (paths) {
68
+ const approxBBox = this.viewport.visibleRect;
69
+ renderer.startObject(approxBBox);
70
+ for (const path of paths) {
71
+ renderer.drawPath(path);
72
+ }
73
+ renderer.endObject();
74
+ }
75
+ }
76
+ build() {
77
+ return new Stroke(this.previewFullPath());
78
+ }
79
+ getMinFit() {
80
+ let minFit = Math.min(this.minFitAllowed, this.averageWidth / 3);
81
+ if (minFit < 1e-10) {
82
+ minFit = this.minFitAllowed;
83
+ }
84
+ return minFit;
85
+ }
86
+ roundPoint(point) {
87
+ const minFit = this.getMinFit();
88
+ return Viewport.roundPoint(point, minFit);
89
+ }
90
+ roundDistance(dist) {
91
+ const minFit = this.getMinFit();
92
+ return Viewport.roundPoint(dist, minFit);
93
+ }
94
+ addPoint(newPoint) {
95
+ this.widthAverageNumSamples++;
96
+ this.averageWidth =
97
+ this.averageWidth * (this.widthAverageNumSamples - 1) / this.widthAverageNumSamples
98
+ + newPoint.width / this.widthAverageNumSamples;
99
+ const roundedPoint = this.roundPoint(newPoint.pos);
100
+ if (!roundedPoint.eq(this.lastPoint)) {
101
+ this.parts.push({
102
+ kind: PathCommandType.LineTo,
103
+ point: this.roundPoint(newPoint.pos),
104
+ });
105
+ this.bbox = this.bbox.grownToPoint(roundedPoint);
106
+ }
107
+ }
108
+ }
@@ -285,7 +285,7 @@ export default class PressureSensitiveFreehandLineBuilder {
285
285
  // Approximate the normal at the location of the control point
286
286
  let projectionT = bezier.project(controlPoint.xy).t;
287
287
  if (!projectionT) {
288
- if (startPt.minus(controlPoint).magnitudeSquared() < endPt.minus(controlPoint).magnitudeSquared()) {
288
+ if (startPt.squareDistanceTo(controlPoint) < endPt.squareDistanceTo(controlPoint)) {
289
289
  projectionT = 0.1;
290
290
  }
291
291
  else {
@@ -83,7 +83,7 @@ class ShapeFitBuilder {
83
83
  // Find the closest point to the startPoint
84
84
  for (let i = 0; i < templatePoints.length; i++) {
85
85
  const current = templatePoints[i];
86
- const currentSqrDist = current.minus(startPoint).magnitudeSquared();
86
+ const currentSqrDist = current.squareDistanceTo(startPoint);
87
87
  if (!closestToFirst || currentSqrDist < closestToFirstSqrDist) {
88
88
  closestToFirstSqrDist = currentSqrDist;
89
89
  closestToFirst = current;
@@ -1,5 +1,6 @@
1
1
  export * from './builders/types';
2
2
  export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
3
+ export { makePolylineBuilder } from './builders/PolylineBuilder';
3
4
  export { makePressureSensitiveFreehandLineBuilder } from './builders/PressureSensitiveFreehandLineBuilder';
4
5
  export { makeOutlinedCircleBuilder } from './builders/CircleBuilder';
5
6
  export { default as StrokeSmoother, Curve as StrokeSmootherCurve } from './util/StrokeSmoother';
@@ -1,5 +1,6 @@
1
1
  export * from './builders/types.mjs';
2
2
  export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder.mjs';
3
+ export { makePolylineBuilder } from './builders/PolylineBuilder.mjs';
3
4
  export { makePressureSensitiveFreehandLineBuilder } from './builders/PressureSensitiveFreehandLineBuilder.mjs';
4
5
  export { makeOutlinedCircleBuilder } from './builders/CircleBuilder.mjs';
5
6
  export { default as StrokeSmoother } from './util/StrokeSmoother.mjs';
@@ -38,8 +38,8 @@ export class StrokeSmoother {
38
38
  const startPt = this.currentCurve.p0;
39
39
  const controlPt = this.currentCurve.p1;
40
40
  const endPt = this.currentCurve.p2;
41
- const toControlDist = startPt.minus(controlPt).length();
42
- const toEndDist = endPt.minus(controlPt).length();
41
+ const toControlDist = startPt.distanceTo(controlPt);
42
+ const toEndDist = endPt.distanceTo(controlPt);
43
43
  return toControlDist + toEndDist;
44
44
  }
45
45
  finalizeCurrentCurve() {
@@ -96,7 +96,7 @@ export class StrokeSmoother {
96
96
  return;
97
97
  }
98
98
  const threshold = Math.min(this.lastPoint.width, newPoint.width) / 3;
99
- const shouldSnapToInitial = this.startPoint.pos.minus(newPoint.pos).magnitude() < threshold
99
+ const shouldSnapToInitial = this.startPoint.pos.distanceTo(newPoint.pos) < threshold
100
100
  && this.isFirstSegment;
101
101
  // Snap to the starting point if the stroke is contained within a small ball centered
102
102
  // at the starting point.
@@ -147,7 +147,7 @@ export class StrokeSmoother {
147
147
  const maxRelativeLength = 1.7;
148
148
  const segmentStart = this.buffer[0];
149
149
  const segmentEnd = newPoint.pos;
150
- const startEndDist = segmentEnd.minus(segmentStart).magnitude();
150
+ const startEndDist = segmentEnd.distanceTo(segmentStart);
151
151
  const maxControlPointDist = maxRelativeLength * startEndDist;
152
152
  // Exit in cases where we would divide by zero
153
153
  if (maxControlPointDist === 0 || exitingVec.magnitude() === 0 || !isFinite(exitingVec.magnitude())) {
@@ -105,7 +105,10 @@ export default class EditorImage {
105
105
  getImportExportRect(): Rect2;
106
106
  /**
107
107
  * Sets the import/export rectangle to the given `imageRect`. Disables
108
- * autoresize (if it was previously enabled).
108
+ * autoresize if it was previously enabled.
109
+ *
110
+ * **Note**: The import/export rectangle is the same as the size of any
111
+ * {@link BackgroundComponent}s (and other components that auto-resize).
109
112
  */
110
113
  setImportExportRect(imageRect: Rect2): SerializableCommand;
111
114
  /** @see {@link setAutoresizeEnabled} */
@@ -205,7 +205,10 @@ class EditorImage {
205
205
  }
206
206
  /**
207
207
  * Sets the import/export rectangle to the given `imageRect`. Disables
208
- * autoresize (if it was previously enabled).
208
+ * autoresize if it was previously enabled.
209
+ *
210
+ * **Note**: The import/export rectangle is the same as the size of any
211
+ * {@link BackgroundComponent}s (and other components that auto-resize).
209
212
  */
210
213
  setImportExportRect(imageRect) {
211
214
  return _a.SetImportExportRectCommand.of(this, imageRect, false);
@@ -53,7 +53,7 @@ export interface KeyUpEvent extends BaseKeyEvent {
53
53
  }
54
54
  export interface CopyEvent {
55
55
  readonly kind: InputEvtType.CopyEvent;
56
- setData(mime: string, data: string): void;
56
+ setData(mime: string, data: string | Promise<Blob>): void;
57
57
  }
58
58
  export interface PasteEvent {
59
59
  readonly kind: InputEvtType.PasteEvent;
@@ -76,7 +76,17 @@ export interface PointerMoveEvt extends PointerEvtBase {
76
76
  export interface PointerUpEvt extends PointerEvtBase {
77
77
  readonly kind: InputEvtType.PointerUpEvt;
78
78
  }
79
+ /**
80
+ * An internal `js-draw` pointer event type.
81
+ *
82
+ * This **is not** the same as a DOM pointer event.
83
+ */
79
84
  export type PointerEvt = PointerDownEvt | PointerMoveEvt | PointerUpEvt;
85
+ /**
86
+ * An internal `js-draw` input event type.
87
+ *
88
+ * These are not DOM events.
89
+ */
80
90
  export type InputEvt = KeyPressEvent | KeyUpEvent | WheelEvt | GestureCancelEvt | PointerEvt | CopyEvent | PasteEvent;
81
91
  export declare const keyUpEventFromHTMLEvent: (event: KeyboardEvent) => KeyUpEvent;
82
92
  export declare const keyPressEventFromHTMLEvent: (event: KeyboardEvent) => KeyPressEvent;
@@ -1,6 +1,9 @@
1
1
  import { EditorLocalization } from '../localization';
2
2
  /**
3
3
  * Comments to help translators create translations.
4
+ *
5
+ * The key for each comment should be the same as is used in the
6
+ * translation and original source records.
4
7
  */
5
8
  declare const comments: Partial<Record<keyof EditorLocalization, string>>;
6
9
  export default comments;
@@ -1,5 +1,8 @@
1
1
  /**
2
2
  * Comments to help translators create translations.
3
+ *
4
+ * The key for each comment should be the same as is used in the
5
+ * translation and original source records.
3
6
  */
4
7
  const comments = {
5
8
  pen: 'Likely unused',
@@ -109,8 +109,6 @@ const localization = {
109
109
  soundExplorer: 'Klangbasierte Bilderkundung',
110
110
  disableAccessibilityExploreTool: 'Deaktiviere klangbasierte Erkundung',
111
111
  enableAccessibilityExploreTool: 'Aktiviere klangbasierte Erkundung',
112
- copied: (count, description) => `${count} ${description} kopiert`,
113
- pasted: (count, description) => `${count} ${description} eingefügt`,
114
112
  unionOf: (actionDescription, actionCount) => `Vereinigung: ${actionCount} ${actionDescription}`,
115
113
  emptyBackground: 'Leerer Hintergrund',
116
114
  filledBackgroundWithColor: (color) => `Gefüllter Hintergrund (${color})`,
@@ -61,8 +61,8 @@ const localization = {
61
61
  toNextMatch: 'Próxima',
62
62
  closeDialog: 'Cerrar',
63
63
  anyDevicePanning: 'Mover la pantalla con todo dispotivo',
64
- copied: (count, description) => `Copied ${count} ${description}`,
65
- pasted: (count, description) => `Pasted ${count} ${description}`,
64
+ copied: (count) => `${count} cosas fueron copiados`,
65
+ pasted: (count) => count === 1 ? 'Pegado' : `${count} cosas fueron pegados`,
66
66
  toolEnabledAnnouncement: (toolName) => `${toolName} fue activado`,
67
67
  toolDisabledAnnouncement: (toolName) => `${toolName} fue desactivado`,
68
68
  resizeOutputCommand: (newSize) => `Tamaño de imagen fue cambiado a ${newSize.w}x${newSize.h}`,
@@ -58,4 +58,11 @@ export default class CanvasRenderer extends AbstractRenderer {
58
58
  endObject(): void;
59
59
  drawPoints(...points: Point2[]): void;
60
60
  isTooSmallToRender(rect: Rect2): boolean;
61
+ static fromViewport(exportViewport: Viewport, options?: {
62
+ canvasSize?: Vec2;
63
+ maxCanvasDimen?: number;
64
+ }): {
65
+ renderer: CanvasRenderer;
66
+ element: HTMLCanvasElement;
67
+ };
61
68
  }
@@ -245,4 +245,20 @@ export default class CanvasRenderer extends AbstractRenderer {
245
245
  const anyTooSmall = Math.abs(diagonal.x) < anyDimenMinSize || Math.abs(diagonal.y) < anyDimenMinSize;
246
246
  return bothTooSmall || anyTooSmall;
247
247
  }
248
+ // @internal
249
+ static fromViewport(exportViewport, options = {}) {
250
+ const canvas = document.createElement('canvas');
251
+ const exportRectSize = exportViewport.getScreenRectSize();
252
+ let canvasSize = options.canvasSize ?? exportRectSize;
253
+ if (options.maxCanvasDimen && canvasSize.maximumEntryMagnitude() > options.maxCanvasDimen) {
254
+ canvasSize = canvasSize.times(options.maxCanvasDimen / canvasSize.maximumEntryMagnitude());
255
+ }
256
+ canvas.width = canvasSize.x;
257
+ canvas.height = canvasSize.y;
258
+ const ctx = canvas.getContext('2d');
259
+ // Scale to ensure that the entire output is visible.
260
+ const scaleFactor = Math.min(canvasSize.x / exportRectSize.x, canvasSize.y / exportRectSize.y);
261
+ ctx.scale(scaleFactor, scaleFactor);
262
+ return { renderer: new CanvasRenderer(ctx, exportViewport), element: canvas };
263
+ }
248
264
  }
@@ -110,7 +110,7 @@ export default class SVGRenderer extends AbstractRenderer {
110
110
  }
111
111
  if (style.stroke) {
112
112
  pathElem.setAttribute('stroke', style.stroke.color.toHexString());
113
- pathElem.setAttribute('stroke-width', toRoundedString(style.stroke.width));
113
+ pathElem.setAttribute('stroke-width', toRoundedString(style.stroke.width * this.getSizeOfCanvasPixelOnScreen()));
114
114
  }
115
115
  this.elem.appendChild(pathElem);
116
116
  this.objectElems?.push(pathElem);
@@ -3,8 +3,9 @@ import TextRenderingStyle from '../rendering/TextRenderingStyle';
3
3
  import { PenStyle } from '../tools/Pen';
4
4
  export type IconElemType = HTMLImageElement | SVGElement;
5
5
  /**
6
- * Provides icons that can be used in the toolbar, etc.
7
- * Extend this class and override methods to customize icons.
6
+ * Provides icons that can be used in the toolbar and other locations.
7
+ *
8
+ * To customize the icons used by the editor, extend this class and override methods.
8
9
  *
9
10
  * @example
10
11
  * ```ts,runnable
@@ -12,7 +13,7 @@ export type IconElemType = HTMLImageElement | SVGElement;
12
13
  *
13
14
  * class CustomIconProvider extends jsdraw.IconProvider {
14
15
  * // Use '☺' instead of the default dropdown symbol.
15
- * public makeDropdownIcon() {
16
+ * public override makeDropdownIcon() {
16
17
  * const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
17
18
  * icon.innerHTML = `
18
19
  * <text x='5' y='55' style='fill: var(--icon-color); font-size: 50pt;'>☺</text>
@@ -24,6 +25,8 @@ export type IconElemType = HTMLImageElement | SVGElement;
24
25
  *
25
26
  * const icons = new CustomIconProvider();
26
27
  * const editor = new jsdraw.Editor(document.body, {
28
+ * // The icon pack to use is specified through the editor's initial
29
+ * // configuration object:
27
30
  * iconProvider: icons,
28
31
  * });
29
32
  *
@@ -57,8 +57,9 @@ const makeRedoIcon = (mirror) => {
57
57
  return icon;
58
58
  };
59
59
  /**
60
- * Provides icons that can be used in the toolbar, etc.
61
- * Extend this class and override methods to customize icons.
60
+ * Provides icons that can be used in the toolbar and other locations.
61
+ *
62
+ * To customize the icons used by the editor, extend this class and override methods.
62
63
  *
63
64
  * @example
64
65
  * ```ts,runnable
@@ -66,7 +67,7 @@ const makeRedoIcon = (mirror) => {
66
67
  *
67
68
  * class CustomIconProvider extends jsdraw.IconProvider {
68
69
  * // Use '☺' instead of the default dropdown symbol.
69
- * public makeDropdownIcon() {
70
+ * public override makeDropdownIcon() {
70
71
  * const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
71
72
  * icon.innerHTML = `
72
73
  * <text x='5' y='55' style='fill: var(--icon-color); font-size: 50pt;'>☺</text>
@@ -78,6 +79,8 @@ const makeRedoIcon = (mirror) => {
78
79
  *
79
80
  * const icons = new CustomIconProvider();
80
81
  * const editor = new jsdraw.Editor(document.body, {
82
+ * // The icon pack to use is specified through the editor's initial
83
+ * // configuration object:
81
84
  * iconProvider: icons,
82
85
  * });
83
86
  *
@@ -92,7 +95,6 @@ class IconProvider {
92
95
  makeUndoIcon() {
93
96
  return makeRedoIcon(true);
94
97
  }
95
- // @param mirror - reflect across the x-axis. This parameter is internal.
96
98
  // @returns a redo icon.
97
99
  makeRedoIcon() {
98
100
  return makeRedoIcon(false);
@@ -166,7 +166,30 @@ class DocumentPropertiesWidget extends BaseWidget {
166
166
  row.replaceChildren(label, input);
167
167
  return {
168
168
  setValue: (value) => {
169
- input.value = value.toString();
169
+ // Slightly improve the case where the user tries to change the
170
+ // first digit of a dimension like 600.
171
+ //
172
+ // As changing the value also gives the image zero size (which is unsupported,
173
+ // .setValue is called immediately). We work around this by trying to select
174
+ // the added/changed digits.
175
+ //
176
+ // See https://github.com/personalizedrefrigerator/js-draw/issues/58.
177
+ if (document.activeElement === input && input.value.match(/^0*$/)) {
178
+ // We need to switch to type="text" and back to type="number" because
179
+ // number inputs don't support selection.
180
+ //
181
+ // See https://stackoverflow.com/q/22381837
182
+ const originalValue = input.value;
183
+ input.type = 'text';
184
+ input.value = value.toString();
185
+ // Select the added digits
186
+ const lengthToSelect = Math.max(1, input.value.length - originalValue.length);
187
+ input.setSelectionRange(0, lengthToSelect);
188
+ input.type = 'number';
189
+ }
190
+ else {
191
+ input.value = value.toString();
192
+ }
170
193
  },
171
194
  setIsAutomaticSize: (automatic) => {
172
195
  input.disabled = automatic;
@@ -15,7 +15,7 @@ export interface PenTypeRecord {
15
15
  export default class PenToolWidget extends BaseToolWidget {
16
16
  private tool;
17
17
  private updateInputs;
18
- protected penTypes: PenTypeRecord[];
18
+ protected penTypes: Readonly<PenTypeRecord>[];
19
19
  protected shapelikeIDs: string[];
20
20
  private static idCounter;
21
21
  constructor(editor: Editor, tool: Pen, localization?: ToolbarLocalization);
@@ -19,8 +19,11 @@ class PenToolWidget extends BaseToolWidget {
19
19
  this.updateInputs = () => { };
20
20
  // Pen types that correspond to
21
21
  this.shapelikeIDs = ['pressure-sensitive-pen', 'freehand-pen'];
22
+ // Additional client-specified pens.
23
+ const additionalPens = editor.getCurrentSettings().pens?.additionalPenTypes ?? [];
22
24
  // Default pen types
23
25
  this.penTypes = [
26
+ // Non-shape pens
24
27
  {
25
28
  name: this.localizationTable.flatTipPen,
26
29
  id: 'pressure-sensitive-pen',
@@ -31,6 +34,8 @@ class PenToolWidget extends BaseToolWidget {
31
34
  id: 'freehand-pen',
32
35
  factory: makeFreehandLineBuilder,
33
36
  },
37
+ ...(additionalPens.filter(pen => !pen.isShapeBuilder)),
38
+ // Shape pens
34
39
  {
35
40
  name: this.localizationTable.arrowPen,
36
41
  id: 'arrow',
@@ -60,7 +65,8 @@ class PenToolWidget extends BaseToolWidget {
60
65
  id: 'outlined-circle',
61
66
  isShapeBuilder: true,
62
67
  factory: makeOutlinedCircleBuilder,
63
- }
68
+ },
69
+ ...(additionalPens.filter(pen => pen.isShapeBuilder)),
64
70
  ];
65
71
  this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
66
72
  if (toolEvt.kind !== EditorEventType.ToolUpdated) {
@@ -45,7 +45,7 @@ export default class Eraser extends BaseTool {
45
45
  return Rect2.fromCorners(centerPoint.minus(halfSize), centerPoint.plus(halfSize));
46
46
  }
47
47
  eraseTo(currentPoint) {
48
- if (!this.isFirstEraseEvt && currentPoint.minus(this.lastPoint).magnitude() === 0) {
48
+ if (!this.isFirstEraseEvt && currentPoint.distanceTo(this.lastPoint) === 0) {
49
49
  return;
50
50
  }
51
51
  this.isFirstEraseEvt = false;
@@ -8,10 +8,10 @@ var StabilizerType;
8
8
  })(StabilizerType || (StabilizerType = {}));
9
9
  const defaultOptions = {
10
10
  kind: StabilizerType.IntertialStabilizer,
11
- mass: 0.4,
12
- springConstant: 100.0,
11
+ mass: 0.4, // kg
12
+ springConstant: 100.0, // N/m
13
13
  frictionCoefficient: 0.28,
14
- maxPointDist: 10,
14
+ maxPointDist: 10, // screen units
15
15
  inertiaFraction: 0.75,
16
16
  minSimilarityToFinalize: 0.0,
17
17
  velocityDecayFactor: 0.1,
@@ -23,8 +23,29 @@ export default class PasteHandler extends BaseTool {
23
23
  // @internal
24
24
  onPaste(event) {
25
25
  const mime = event.mime.toLowerCase();
26
- if (mime === 'image/svg+xml') {
27
- void this.doSVGPaste(event.data);
26
+ const svgData = (() => {
27
+ if (mime === 'image/svg+xml') {
28
+ return event.data;
29
+ }
30
+ if (mime !== 'text/html') {
31
+ return false;
32
+ }
33
+ // text/html is sometimes handlable SVG data. Use a hueristic
34
+ // to determine if this is the case:
35
+ // We use [^] and not . so that newlines are included.
36
+ const match = event.data.match(/^[^]{0,200}<svg.*/i); // [^]{0,200} <- Allow for metadata near start
37
+ if (!match) {
38
+ return false;
39
+ }
40
+ // Extract the SVG element from the pasted data
41
+ let svgEnd = event.data.toLowerCase().lastIndexOf('</svg>');
42
+ if (svgEnd === -1)
43
+ svgEnd = event.data.length;
44
+ return event.data.substring(event.data.search(/<svg/i), svgEnd);
45
+ })();
46
+ if (svgData) {
47
+ console.log('svgpaste', svgData);
48
+ void this.doSVGPaste(svgData);
28
49
  return true;
29
50
  }
30
51
  else if (mime === 'text/plain') {
@@ -38,16 +59,21 @@ export default class PasteHandler extends BaseTool {
38
59
  return false;
39
60
  }
40
61
  async addComponentsFromPaste(components) {
41
- await this.editor.addAndCenterComponents(components);
62
+ await this.editor.addAndCenterComponents(components, true, this.editor.localization.pasted(components.length));
42
63
  }
43
64
  async doSVGPaste(data) {
44
- const sanitize = true;
45
- const loader = SVGLoader.fromString(data, sanitize);
46
- const components = [];
47
- await loader.start((component) => {
48
- components.push(component);
49
- }, (_countProcessed, _totalToProcess) => null);
50
- await this.addComponentsFromPaste(components);
65
+ this.editor.showLoadingWarning(0);
66
+ try {
67
+ const loader = SVGLoader.fromString(data, true);
68
+ const components = [];
69
+ await loader.start((component) => {
70
+ components.push(component);
71
+ }, (_countProcessed, _totalToProcess) => null);
72
+ await this.addComponentsFromPaste(components);
73
+ }
74
+ finally {
75
+ this.editor.hideLoadingWarning();
76
+ }
51
77
  }
52
78
  async doTextPaste(text) {
53
79
  const textTools = this.editor.toolController.getMatchingTools(TextTool);
@@ -97,8 +97,8 @@ export default class Pen extends BaseTool {
97
97
  this.currentDeviceType = current.device;
98
98
  if (this.shapeAutocompletionEnabled) {
99
99
  const stationaryDetectionConfig = {
100
- maxSpeed: 8.5,
101
- maxRadius: 11,
100
+ maxSpeed: 8.5, // screenPx/s
101
+ maxRadius: 11, // screenPx
102
102
  minTimeSeconds: 0.5, // s
103
103
  };
104
104
  this.stationaryDetector = new StationaryPenDetector(current, stationaryDetectionConfig, pointer => this.autocorrectShape(pointer));
@@ -2,6 +2,7 @@ import { Mat33, Vec2 } from '@js-draw/math';
2
2
  import { EditorEventType } from '../../types.mjs';
3
3
  import Viewport from '../../Viewport.mjs';
4
4
  import BaseTool from '../BaseTool.mjs';
5
+ import CanvasRenderer from '../../rendering/renderers/CanvasRenderer.mjs';
5
6
  import SVGRenderer from '../../rendering/renderers/SVGRenderer.mjs';
6
7
  import Selection from './Selection.mjs';
7
8
  import TextComponent from '../../components/TextComponent.mjs';
@@ -370,19 +371,37 @@ class SelectionTool extends BaseTool {
370
371
  return false;
371
372
  }
372
373
  const exportViewport = new Viewport(() => { });
373
- exportViewport.updateScreenSize(Vec2.of(bbox.w, bbox.h));
374
- exportViewport.resetTransform(Mat33.translation(bbox.topLeft.times(-1)));
375
- const sanitize = true;
376
- const { element: svgExportElem, renderer: svgRenderer } = SVGRenderer.fromViewport(exportViewport, sanitize);
374
+ const selectionScreenSize = this.selectionBox.getScreenRegion().size.times(this.editor.display.getDevicePixelRatio());
375
+ // Update the viewport to have screen size roughly equal to the size of the selection box
376
+ let scaleFactor = selectionScreenSize.maximumEntryMagnitude() / (bbox.size.maximumEntryMagnitude() || 1);
377
+ // Round to a nearby power of two
378
+ scaleFactor = Math.pow(2, Math.ceil(Math.log2(scaleFactor)));
379
+ exportViewport.updateScreenSize(bbox.size.times(scaleFactor));
380
+ exportViewport.resetTransform(Mat33.scaling2D(scaleFactor)
381
+ // Move the selection onto the screen
382
+ .rightMul(Mat33.translation(bbox.topLeft.times(-1))));
383
+ const { element: svgExportElem, renderer: svgRenderer } = SVGRenderer.fromViewport(exportViewport, { sanitize: true, useViewBoxForPositioning: true });
384
+ const { element: canvas, renderer: canvasRenderer } = CanvasRenderer.fromViewport(exportViewport, { maxCanvasDimen: 4096 });
377
385
  const text = [];
378
386
  for (const elem of selectedElems) {
379
387
  elem.render(svgRenderer);
388
+ elem.render(canvasRenderer);
380
389
  if (elem instanceof TextComponent) {
381
390
  text.push(elem.getText());
382
391
  }
383
392
  }
384
393
  event.setData('image/svg+xml', svgExportElem.outerHTML);
385
394
  event.setData('text/html', svgExportElem.outerHTML);
395
+ event.setData('image/png', new Promise((resolve, reject) => {
396
+ canvas.toBlob((blob) => {
397
+ if (blob) {
398
+ resolve(blob);
399
+ }
400
+ else {
401
+ reject('Failed to convert canvas to blob.');
402
+ }
403
+ }, 'image/png');
404
+ }));
386
405
  if (text.length > 0) {
387
406
  event.setData('text/plain', text.join('\n'));
388
407
  }
@@ -23,7 +23,7 @@ export default class ToPointerAutoscroller {
23
23
  return Vec2.zero;
24
24
  }
25
25
  const closestEdgePoint = autoscrollBoundary.getClosestPointOnBoundaryTo(screenPoint);
26
- const distToEdge = closestEdgePoint.minus(screenPoint).magnitude();
26
+ const distToEdge = closestEdgePoint.distanceTo(screenPoint);
27
27
  const toEdge = closestEdgePoint.minus(screenPoint);
28
28
  // Go faster for points further away from the boundary.
29
29
  const maximumScaleFactor = 1.25;