js-draw 1.16.0 → 1.17.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (103) hide show
  1. package/dist/Editor.css +11 -0
  2. package/dist/bundle.js +2 -2
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/Editor.d.ts +79 -6
  5. package/dist/cjs/Editor.js +114 -91
  6. package/dist/cjs/Pointer.d.ts +2 -1
  7. package/dist/cjs/Pointer.js +9 -2
  8. package/dist/cjs/commands/localization.d.ts +1 -0
  9. package/dist/cjs/commands/localization.js +1 -0
  10. package/dist/cjs/commands/uniteCommands.d.ts +5 -1
  11. package/dist/cjs/commands/uniteCommands.js +33 -7
  12. package/dist/cjs/components/TextComponent.d.ts +36 -1
  13. package/dist/cjs/components/TextComponent.js +39 -1
  14. package/dist/cjs/components/builders/ArrowBuilder.js +1 -1
  15. package/dist/cjs/components/builders/PolylineBuilder.d.ts +35 -0
  16. package/dist/cjs/components/builders/PolylineBuilder.js +115 -0
  17. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +1 -1
  18. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +1 -1
  19. package/dist/cjs/components/lib.d.ts +1 -0
  20. package/dist/cjs/components/lib.js +3 -1
  21. package/dist/cjs/components/util/StrokeSmoother.js +4 -4
  22. package/dist/cjs/image/EditorImage.d.ts +4 -1
  23. package/dist/cjs/image/EditorImage.js +4 -1
  24. package/dist/cjs/inputEvents.d.ts +11 -1
  25. package/dist/cjs/localizations/comments.d.ts +3 -0
  26. package/dist/cjs/localizations/comments.js +3 -0
  27. package/dist/cjs/localizations/de.js +0 -2
  28. package/dist/cjs/localizations/es.js +2 -2
  29. package/dist/cjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
  30. package/dist/cjs/rendering/renderers/CanvasRenderer.js +16 -0
  31. package/dist/cjs/rendering/renderers/SVGRenderer.js +1 -1
  32. package/dist/cjs/toolbar/IconProvider.d.ts +6 -3
  33. package/dist/cjs/toolbar/IconProvider.js +6 -4
  34. package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +24 -1
  35. package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
  36. package/dist/cjs/toolbar/widgets/PenToolWidget.js +7 -1
  37. package/dist/cjs/tools/Eraser.js +1 -1
  38. package/dist/cjs/tools/InputFilter/InputStabilizer.js +3 -3
  39. package/dist/cjs/tools/PasteHandler.js +40 -10
  40. package/dist/cjs/tools/Pen.js +2 -2
  41. package/dist/cjs/tools/SelectionTool/SelectionTool.js +23 -4
  42. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +1 -1
  43. package/dist/cjs/tools/ToolController.d.ts +17 -1
  44. package/dist/cjs/tools/ToolController.js +21 -8
  45. package/dist/cjs/tools/localization.d.ts +2 -2
  46. package/dist/cjs/tools/localization.js +2 -2
  47. package/dist/cjs/util/ClipboardHandler.d.ts +27 -0
  48. package/dist/cjs/util/ClipboardHandler.js +205 -0
  49. package/dist/cjs/util/ClipboardHandler.test.d.ts +1 -0
  50. package/dist/cjs/version.d.ts +5 -0
  51. package/dist/cjs/version.js +6 -1
  52. package/dist/mjs/Editor.d.ts +79 -6
  53. package/dist/mjs/Editor.mjs +114 -91
  54. package/dist/mjs/Pointer.d.ts +2 -1
  55. package/dist/mjs/Pointer.mjs +9 -2
  56. package/dist/mjs/commands/localization.d.ts +1 -0
  57. package/dist/mjs/commands/localization.mjs +1 -0
  58. package/dist/mjs/commands/uniteCommands.d.ts +5 -1
  59. package/dist/mjs/commands/uniteCommands.mjs +33 -7
  60. package/dist/mjs/components/TextComponent.d.ts +36 -1
  61. package/dist/mjs/components/TextComponent.mjs +40 -2
  62. package/dist/mjs/components/builders/ArrowBuilder.mjs +1 -1
  63. package/dist/mjs/components/builders/PolylineBuilder.d.ts +35 -0
  64. package/dist/mjs/components/builders/PolylineBuilder.mjs +108 -0
  65. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +1 -1
  66. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +1 -1
  67. package/dist/mjs/components/lib.d.ts +1 -0
  68. package/dist/mjs/components/lib.mjs +1 -0
  69. package/dist/mjs/components/util/StrokeSmoother.mjs +4 -4
  70. package/dist/mjs/image/EditorImage.d.ts +4 -1
  71. package/dist/mjs/image/EditorImage.mjs +4 -1
  72. package/dist/mjs/inputEvents.d.ts +11 -1
  73. package/dist/mjs/localizations/comments.d.ts +3 -0
  74. package/dist/mjs/localizations/comments.mjs +3 -0
  75. package/dist/mjs/localizations/de.mjs +0 -2
  76. package/dist/mjs/localizations/es.mjs +2 -2
  77. package/dist/mjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
  78. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +16 -0
  79. package/dist/mjs/rendering/renderers/SVGRenderer.mjs +1 -1
  80. package/dist/mjs/toolbar/IconProvider.d.ts +6 -3
  81. package/dist/mjs/toolbar/IconProvider.mjs +6 -4
  82. package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +24 -1
  83. package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
  84. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +7 -1
  85. package/dist/mjs/tools/Eraser.mjs +1 -1
  86. package/dist/mjs/tools/InputFilter/InputStabilizer.mjs +3 -3
  87. package/dist/mjs/tools/PasteHandler.mjs +40 -10
  88. package/dist/mjs/tools/Pen.mjs +2 -2
  89. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +23 -4
  90. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +1 -1
  91. package/dist/mjs/tools/ToolController.d.ts +17 -1
  92. package/dist/mjs/tools/ToolController.mjs +21 -8
  93. package/dist/mjs/tools/localization.d.ts +2 -2
  94. package/dist/mjs/tools/localization.mjs +2 -2
  95. package/dist/mjs/util/ClipboardHandler.d.ts +27 -0
  96. package/dist/mjs/util/ClipboardHandler.mjs +200 -0
  97. package/dist/mjs/util/ClipboardHandler.test.d.ts +1 -0
  98. package/dist/mjs/version.d.ts +5 -0
  99. package/dist/mjs/version.mjs +6 -1
  100. package/package.json +6 -6
  101. package/src/Editor.scss +10 -0
  102. package/src/toolbar/EdgeToolbar.scss +2 -0
  103. package/src/tools/SoundUITool.scss +4 -1
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.makePolylineBuilder = void 0;
7
+ const math_1 = require("@js-draw/math");
8
+ const Stroke_1 = __importDefault(require("../Stroke"));
9
+ const Viewport_1 = __importDefault(require("../../Viewport"));
10
+ const makeShapeFitAutocorrect_1 = __importDefault(require("./autocorrect/makeShapeFitAutocorrect"));
11
+ /**
12
+ * Creates strokes from line segments rather than Bézier curves.
13
+ *
14
+ * @beta Output behavior may change significantly between versions. For now, intended for debugging.
15
+ */
16
+ exports.makePolylineBuilder = (0, makeShapeFitAutocorrect_1.default)((initialPoint, viewport) => {
17
+ const minFit = viewport.getSizeOfPixelOnCanvas();
18
+ return new PolylineBuilder(initialPoint, minFit, viewport);
19
+ });
20
+ class PolylineBuilder {
21
+ constructor(startPoint, minFitAllowed, viewport) {
22
+ this.minFitAllowed = minFitAllowed;
23
+ this.viewport = viewport;
24
+ this.parts = [];
25
+ this.widthAverageNumSamples = 1;
26
+ this.averageWidth = startPoint.width;
27
+ this.startPoint = {
28
+ ...startPoint,
29
+ pos: this.roundPoint(startPoint.pos),
30
+ };
31
+ this.lastPoint = this.startPoint.pos;
32
+ this.bbox = new math_1.Rect2(this.startPoint.pos.x, this.startPoint.pos.y, 0, 0);
33
+ this.parts = [
34
+ {
35
+ kind: math_1.PathCommandType.MoveTo,
36
+ point: this.startPoint.pos,
37
+ },
38
+ ];
39
+ }
40
+ getBBox() {
41
+ return this.bbox.grownBy(this.averageWidth);
42
+ }
43
+ getRenderingStyle() {
44
+ return {
45
+ fill: math_1.Color4.transparent,
46
+ stroke: {
47
+ color: this.startPoint.color,
48
+ width: this.roundDistance(this.averageWidth),
49
+ }
50
+ };
51
+ }
52
+ previewCurrentPath() {
53
+ const startPoint = this.startPoint.pos;
54
+ const commands = [...this.parts];
55
+ // TODO: For now, this is necesary for the path to be visible.
56
+ if (commands.length <= 1) {
57
+ commands.push({
58
+ kind: math_1.PathCommandType.LineTo,
59
+ point: startPoint,
60
+ });
61
+ }
62
+ return {
63
+ startPoint,
64
+ commands,
65
+ style: this.getRenderingStyle(),
66
+ };
67
+ }
68
+ previewFullPath() {
69
+ return [this.previewCurrentPath()];
70
+ }
71
+ preview(renderer) {
72
+ const paths = this.previewFullPath();
73
+ if (paths) {
74
+ const approxBBox = this.viewport.visibleRect;
75
+ renderer.startObject(approxBBox);
76
+ for (const path of paths) {
77
+ renderer.drawPath(path);
78
+ }
79
+ renderer.endObject();
80
+ }
81
+ }
82
+ build() {
83
+ return new Stroke_1.default(this.previewFullPath());
84
+ }
85
+ getMinFit() {
86
+ let minFit = Math.min(this.minFitAllowed, this.averageWidth / 3);
87
+ if (minFit < 1e-10) {
88
+ minFit = this.minFitAllowed;
89
+ }
90
+ return minFit;
91
+ }
92
+ roundPoint(point) {
93
+ const minFit = this.getMinFit();
94
+ return Viewport_1.default.roundPoint(point, minFit);
95
+ }
96
+ roundDistance(dist) {
97
+ const minFit = this.getMinFit();
98
+ return Viewport_1.default.roundPoint(dist, minFit);
99
+ }
100
+ addPoint(newPoint) {
101
+ this.widthAverageNumSamples++;
102
+ this.averageWidth =
103
+ this.averageWidth * (this.widthAverageNumSamples - 1) / this.widthAverageNumSamples
104
+ + newPoint.width / this.widthAverageNumSamples;
105
+ const roundedPoint = this.roundPoint(newPoint.pos);
106
+ if (!roundedPoint.eq(this.lastPoint)) {
107
+ this.parts.push({
108
+ kind: math_1.PathCommandType.LineTo,
109
+ point: this.roundPoint(newPoint.pos),
110
+ });
111
+ this.bbox = this.bbox.grownToPoint(roundedPoint);
112
+ }
113
+ }
114
+ }
115
+ exports.default = PolylineBuilder;
@@ -291,7 +291,7 @@ class PressureSensitiveFreehandLineBuilder {
291
291
  // Approximate the normal at the location of the control point
292
292
  let projectionT = bezier.project(controlPoint.xy).t;
293
293
  if (!projectionT) {
294
- if (startPt.minus(controlPoint).magnitudeSquared() < endPt.minus(controlPoint).magnitudeSquared()) {
294
+ if (startPt.squareDistanceTo(controlPoint) < endPt.squareDistanceTo(controlPoint)) {
295
295
  projectionT = 0.1;
296
296
  }
297
297
  else {
@@ -85,7 +85,7 @@ class ShapeFitBuilder {
85
85
  // Find the closest point to the startPoint
86
86
  for (let i = 0; i < templatePoints.length; i++) {
87
87
  const current = templatePoints[i];
88
- const currentSqrDist = current.minus(startPoint).magnitudeSquared();
88
+ const currentSqrDist = current.squareDistanceTo(startPoint);
89
89
  if (!closestToFirst || currentSqrDist < closestToFirstSqrDist) {
90
90
  closestToFirstSqrDist = currentSqrDist;
91
91
  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';
@@ -29,10 +29,12 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
29
29
  return (mod && mod.__esModule) ? mod : { "default": mod };
30
30
  };
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.ImageComponent = exports.BackgroundComponentBackgroundType = exports.BackgroundComponent = exports.StrokeComponent = exports.Text = exports.TextComponent = exports.isRestylableComponent = exports.createRestyleComponentCommand = exports.Stroke = exports.AbstractComponent = exports.StrokeSmoother = exports.makeOutlinedCircleBuilder = exports.makePressureSensitiveFreehandLineBuilder = exports.makeFreehandLineBuilder = void 0;
32
+ exports.ImageComponent = exports.BackgroundComponentBackgroundType = exports.BackgroundComponent = exports.StrokeComponent = exports.Text = exports.TextComponent = exports.isRestylableComponent = exports.createRestyleComponentCommand = exports.Stroke = exports.AbstractComponent = exports.StrokeSmoother = exports.makeOutlinedCircleBuilder = exports.makePressureSensitiveFreehandLineBuilder = exports.makePolylineBuilder = exports.makeFreehandLineBuilder = void 0;
33
33
  __exportStar(require("./builders/types"), exports);
34
34
  var FreehandLineBuilder_1 = require("./builders/FreehandLineBuilder");
35
35
  Object.defineProperty(exports, "makeFreehandLineBuilder", { enumerable: true, get: function () { return FreehandLineBuilder_1.makeFreehandLineBuilder; } });
36
+ var PolylineBuilder_1 = require("./builders/PolylineBuilder");
37
+ Object.defineProperty(exports, "makePolylineBuilder", { enumerable: true, get: function () { return PolylineBuilder_1.makePolylineBuilder; } });
36
38
  var PressureSensitiveFreehandLineBuilder_1 = require("./builders/PressureSensitiveFreehandLineBuilder");
37
39
  Object.defineProperty(exports, "makePressureSensitiveFreehandLineBuilder", { enumerable: true, get: function () { return PressureSensitiveFreehandLineBuilder_1.makePressureSensitiveFreehandLineBuilder; } });
38
40
  var CircleBuilder_1 = require("./builders/CircleBuilder");
@@ -41,8 +41,8 @@ class StrokeSmoother {
41
41
  const startPt = this.currentCurve.p0;
42
42
  const controlPt = this.currentCurve.p1;
43
43
  const endPt = this.currentCurve.p2;
44
- const toControlDist = startPt.minus(controlPt).length();
45
- const toEndDist = endPt.minus(controlPt).length();
44
+ const toControlDist = startPt.distanceTo(controlPt);
45
+ const toEndDist = endPt.distanceTo(controlPt);
46
46
  return toControlDist + toEndDist;
47
47
  }
48
48
  finalizeCurrentCurve() {
@@ -99,7 +99,7 @@ class StrokeSmoother {
99
99
  return;
100
100
  }
101
101
  const threshold = Math.min(this.lastPoint.width, newPoint.width) / 3;
102
- const shouldSnapToInitial = this.startPoint.pos.minus(newPoint.pos).magnitude() < threshold
102
+ const shouldSnapToInitial = this.startPoint.pos.distanceTo(newPoint.pos) < threshold
103
103
  && this.isFirstSegment;
104
104
  // Snap to the starting point if the stroke is contained within a small ball centered
105
105
  // at the starting point.
@@ -150,7 +150,7 @@ class StrokeSmoother {
150
150
  const maxRelativeLength = 1.7;
151
151
  const segmentStart = this.buffer[0];
152
152
  const segmentEnd = newPoint.pos;
153
- const startEndDist = segmentEnd.minus(segmentStart).magnitude();
153
+ const startEndDist = segmentEnd.distanceTo(segmentStart);
154
154
  const maxControlPointDist = maxRelativeLength * startEndDist;
155
155
  // Exit in cases where we would divide by zero
156
156
  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} */
@@ -235,7 +235,10 @@ class EditorImage {
235
235
  }
236
236
  /**
237
237
  * Sets the import/export rectangle to the given `imageRect`. Disables
238
- * autoresize (if it was previously enabled).
238
+ * autoresize if it was previously enabled.
239
+ *
240
+ * **Note**: The import/export rectangle is the same as the size of any
241
+ * {@link BackgroundComponent}s (and other components that auto-resize).
239
242
  */
240
243
  setImportExportRect(imageRect) {
241
244
  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;
@@ -2,6 +2,9 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  /**
4
4
  * Comments to help translators create translations.
5
+ *
6
+ * The key for each comment should be the same as is used in the
7
+ * translation and original source records.
5
8
  */
6
9
  const comments = {
7
10
  pen: 'Likely unused',
@@ -111,8 +111,6 @@ const localization = {
111
111
  soundExplorer: 'Klangbasierte Bilderkundung',
112
112
  disableAccessibilityExploreTool: 'Deaktiviere klangbasierte Erkundung',
113
113
  enableAccessibilityExploreTool: 'Aktiviere klangbasierte Erkundung',
114
- copied: (count, description) => `${count} ${description} kopiert`,
115
- pasted: (count, description) => `${count} ${description} eingefügt`,
116
114
  unionOf: (actionDescription, actionCount) => `Vereinigung: ${actionCount} ${actionDescription}`,
117
115
  emptyBackground: 'Leerer Hintergrund',
118
116
  filledBackgroundWithColor: (color) => `Gefüllter Hintergrund (${color})`,
@@ -63,8 +63,8 @@ const localization = {
63
63
  toNextMatch: 'Próxima',
64
64
  closeDialog: 'Cerrar',
65
65
  anyDevicePanning: 'Mover la pantalla con todo dispotivo',
66
- copied: (count, description) => `Copied ${count} ${description}`,
67
- pasted: (count, description) => `Pasted ${count} ${description}`,
66
+ copied: (count) => `${count} cosas fueron copiados`,
67
+ pasted: (count) => count === 1 ? 'Pegado' : `${count} cosas fueron pegados`,
68
68
  toolEnabledAnnouncement: (toolName) => `${toolName} fue activado`,
69
69
  toolDisabledAnnouncement: (toolName) => `${toolName} fue desactivado`,
70
70
  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
  }
@@ -250,5 +250,21 @@ class CanvasRenderer extends AbstractRenderer_1.default {
250
250
  const anyTooSmall = Math.abs(diagonal.x) < anyDimenMinSize || Math.abs(diagonal.y) < anyDimenMinSize;
251
251
  return bothTooSmall || anyTooSmall;
252
252
  }
253
+ // @internal
254
+ static fromViewport(exportViewport, options = {}) {
255
+ const canvas = document.createElement('canvas');
256
+ const exportRectSize = exportViewport.getScreenRectSize();
257
+ let canvasSize = options.canvasSize ?? exportRectSize;
258
+ if (options.maxCanvasDimen && canvasSize.maximumEntryMagnitude() > options.maxCanvasDimen) {
259
+ canvasSize = canvasSize.times(options.maxCanvasDimen / canvasSize.maximumEntryMagnitude());
260
+ }
261
+ canvas.width = canvasSize.x;
262
+ canvas.height = canvasSize.y;
263
+ const ctx = canvas.getContext('2d');
264
+ // Scale to ensure that the entire output is visible.
265
+ const scaleFactor = Math.min(canvasSize.x / exportRectSize.x, canvasSize.y / exportRectSize.y);
266
+ ctx.scale(scaleFactor, scaleFactor);
267
+ return { renderer: new CanvasRenderer(ctx, exportViewport), element: canvas };
268
+ }
253
269
  }
254
270
  exports.default = CanvasRenderer;
@@ -116,7 +116,7 @@ class SVGRenderer extends AbstractRenderer_1.default {
116
116
  }
117
117
  if (style.stroke) {
118
118
  pathElem.setAttribute('stroke', style.stroke.color.toHexString());
119
- pathElem.setAttribute('stroke-width', (0, math_1.toRoundedString)(style.stroke.width));
119
+ pathElem.setAttribute('stroke-width', (0, math_1.toRoundedString)(style.stroke.width * this.getSizeOfCanvasPixelOnScreen()));
120
120
  }
121
121
  this.elem.appendChild(pathElem);
122
122
  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
  *
@@ -62,8 +62,9 @@ const makeRedoIcon = (mirror) => {
62
62
  return icon;
63
63
  };
64
64
  /**
65
- * Provides icons that can be used in the toolbar, etc.
66
- * Extend this class and override methods to customize icons.
65
+ * Provides icons that can be used in the toolbar and other locations.
66
+ *
67
+ * To customize the icons used by the editor, extend this class and override methods.
67
68
  *
68
69
  * @example
69
70
  * ```ts,runnable
@@ -71,7 +72,7 @@ const makeRedoIcon = (mirror) => {
71
72
  *
72
73
  * class CustomIconProvider extends jsdraw.IconProvider {
73
74
  * // Use '☺' instead of the default dropdown symbol.
74
- * public makeDropdownIcon() {
75
+ * public override makeDropdownIcon() {
75
76
  * const icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
76
77
  * icon.innerHTML = `
77
78
  * <text x='5' y='55' style='fill: var(--icon-color); font-size: 50pt;'>☺</text>
@@ -83,6 +84,8 @@ const makeRedoIcon = (mirror) => {
83
84
  *
84
85
  * const icons = new CustomIconProvider();
85
86
  * const editor = new jsdraw.Editor(document.body, {
87
+ * // The icon pack to use is specified through the editor's initial
88
+ * // configuration object:
86
89
  * iconProvider: icons,
87
90
  * });
88
91
  *
@@ -97,7 +100,6 @@ class IconProvider {
97
100
  makeUndoIcon() {
98
101
  return makeRedoIcon(true);
99
102
  }
100
- // @param mirror - reflect across the x-axis. This parameter is internal.
101
103
  // @returns a redo icon.
102
104
  makeRedoIcon() {
103
105
  return makeRedoIcon(false);
@@ -194,7 +194,30 @@ class DocumentPropertiesWidget extends BaseWidget_1.default {
194
194
  row.replaceChildren(label, input);
195
195
  return {
196
196
  setValue: (value) => {
197
- input.value = value.toString();
197
+ // Slightly improve the case where the user tries to change the
198
+ // first digit of a dimension like 600.
199
+ //
200
+ // As changing the value also gives the image zero size (which is unsupported,
201
+ // .setValue is called immediately). We work around this by trying to select
202
+ // the added/changed digits.
203
+ //
204
+ // See https://github.com/personalizedrefrigerator/js-draw/issues/58.
205
+ if (document.activeElement === input && input.value.match(/^0*$/)) {
206
+ // We need to switch to type="text" and back to type="number" because
207
+ // number inputs don't support selection.
208
+ //
209
+ // See https://stackoverflow.com/q/22381837
210
+ const originalValue = input.value;
211
+ input.type = 'text';
212
+ input.value = value.toString();
213
+ // Select the added digits
214
+ const lengthToSelect = Math.max(1, input.value.length - originalValue.length);
215
+ input.setSelectionRange(0, lengthToSelect);
216
+ input.type = 'number';
217
+ }
218
+ else {
219
+ input.value = value.toString();
220
+ }
198
221
  },
199
222
  setIsAutomaticSize: (automatic) => {
200
223
  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);
@@ -24,8 +24,11 @@ class PenToolWidget extends BaseToolWidget_1.default {
24
24
  this.updateInputs = () => { };
25
25
  // Pen types that correspond to
26
26
  this.shapelikeIDs = ['pressure-sensitive-pen', 'freehand-pen'];
27
+ // Additional client-specified pens.
28
+ const additionalPens = editor.getCurrentSettings().pens?.additionalPenTypes ?? [];
27
29
  // Default pen types
28
30
  this.penTypes = [
31
+ // Non-shape pens
29
32
  {
30
33
  name: this.localizationTable.flatTipPen,
31
34
  id: 'pressure-sensitive-pen',
@@ -36,6 +39,8 @@ class PenToolWidget extends BaseToolWidget_1.default {
36
39
  id: 'freehand-pen',
37
40
  factory: FreehandLineBuilder_1.makeFreehandLineBuilder,
38
41
  },
42
+ ...(additionalPens.filter(pen => !pen.isShapeBuilder)),
43
+ // Shape pens
39
44
  {
40
45
  name: this.localizationTable.arrowPen,
41
46
  id: 'arrow',
@@ -65,7 +70,8 @@ class PenToolWidget extends BaseToolWidget_1.default {
65
70
  id: 'outlined-circle',
66
71
  isShapeBuilder: true,
67
72
  factory: CircleBuilder_1.makeOutlinedCircleBuilder,
68
- }
73
+ },
74
+ ...(additionalPens.filter(pen => pen.isShapeBuilder)),
69
75
  ];
70
76
  this.editor.notifier.on(types_1.EditorEventType.ToolUpdated, toolEvt => {
71
77
  if (toolEvt.kind !== types_1.EditorEventType.ToolUpdated) {
@@ -50,7 +50,7 @@ class Eraser extends BaseTool_1.default {
50
50
  return math_1.Rect2.fromCorners(centerPoint.minus(halfSize), centerPoint.plus(halfSize));
51
51
  }
52
52
  eraseTo(currentPoint) {
53
- if (!this.isFirstEraseEvt && currentPoint.minus(this.lastPoint).magnitude() === 0) {
53
+ if (!this.isFirstEraseEvt && currentPoint.distanceTo(this.lastPoint) === 0) {
54
54
  return;
55
55
  }
56
56
  this.isFirstEraseEvt = false;
@@ -13,10 +13,10 @@ var StabilizerType;
13
13
  })(StabilizerType || (StabilizerType = {}));
14
14
  const defaultOptions = {
15
15
  kind: StabilizerType.IntertialStabilizer,
16
- mass: 0.4,
17
- springConstant: 100.0,
16
+ mass: 0.4, // kg
17
+ springConstant: 100.0, // N/m
18
18
  frictionCoefficient: 0.28,
19
- maxPointDist: 10,
19
+ maxPointDist: 10, // screen units
20
20
  inertiaFraction: 0.75,
21
21
  minSimilarityToFinalize: 0.0,
22
22
  velocityDecayFactor: 0.1,
@@ -28,8 +28,29 @@ class PasteHandler extends BaseTool_1.default {
28
28
  // @internal
29
29
  onPaste(event) {
30
30
  const mime = event.mime.toLowerCase();
31
- if (mime === 'image/svg+xml') {
32
- void this.doSVGPaste(event.data);
31
+ const svgData = (() => {
32
+ if (mime === 'image/svg+xml') {
33
+ return event.data;
34
+ }
35
+ if (mime !== 'text/html') {
36
+ return false;
37
+ }
38
+ // text/html is sometimes handlable SVG data. Use a hueristic
39
+ // to determine if this is the case:
40
+ // We use [^] and not . so that newlines are included.
41
+ const match = event.data.match(/^[^]{0,200}<svg.*/i); // [^]{0,200} <- Allow for metadata near start
42
+ if (!match) {
43
+ return false;
44
+ }
45
+ // Extract the SVG element from the pasted data
46
+ let svgEnd = event.data.toLowerCase().lastIndexOf('</svg>');
47
+ if (svgEnd === -1)
48
+ svgEnd = event.data.length;
49
+ return event.data.substring(event.data.search(/<svg/i), svgEnd);
50
+ })();
51
+ if (svgData) {
52
+ console.log('svgpaste', svgData);
53
+ void this.doSVGPaste(svgData);
33
54
  return true;
34
55
  }
35
56
  else if (mime === 'text/plain') {
@@ -43,16 +64,21 @@ class PasteHandler extends BaseTool_1.default {
43
64
  return false;
44
65
  }
45
66
  async addComponentsFromPaste(components) {
46
- await this.editor.addAndCenterComponents(components);
67
+ await this.editor.addAndCenterComponents(components, true, this.editor.localization.pasted(components.length));
47
68
  }
48
69
  async doSVGPaste(data) {
49
- const sanitize = true;
50
- const loader = SVGLoader_1.default.fromString(data, sanitize);
51
- const components = [];
52
- await loader.start((component) => {
53
- components.push(component);
54
- }, (_countProcessed, _totalToProcess) => null);
55
- await this.addComponentsFromPaste(components);
70
+ this.editor.showLoadingWarning(0);
71
+ try {
72
+ const loader = SVGLoader_1.default.fromString(data, true);
73
+ const components = [];
74
+ await loader.start((component) => {
75
+ components.push(component);
76
+ }, (_countProcessed, _totalToProcess) => null);
77
+ await this.addComponentsFromPaste(components);
78
+ }
79
+ finally {
80
+ this.editor.hideLoadingWarning();
81
+ }
56
82
  }
57
83
  async doTextPaste(text) {
58
84
  const textTools = this.editor.toolController.getMatchingTools(TextTool_1.default);
@@ -67,6 +93,10 @@ class PasteHandler extends BaseTool_1.default {
67
93
  });
68
94
  const defaultTextStyle = { size: 12, fontFamily: 'sans', renderingStyle: { fill: math_1.Color4.red } };
69
95
  const pastedTextStyle = textTools[0]?.getTextStyle() ?? defaultTextStyle;
96
+ // Don't paste text that would be invisible.
97
+ if (text.trim() === '') {
98
+ return;
99
+ }
70
100
  const lines = text.split('\n');
71
101
  await this.addComponentsFromPaste([TextComponent_1.default.fromLines(lines, math_1.Mat33.identity, pastedTextStyle)]);
72
102
  }
@@ -102,8 +102,8 @@ class Pen extends BaseTool_1.default {
102
102
  this.currentDeviceType = current.device;
103
103
  if (this.shapeAutocompletionEnabled) {
104
104
  const stationaryDetectionConfig = {
105
- maxSpeed: 8.5,
106
- maxRadius: 11,
105
+ maxSpeed: 8.5, // screenPx/s
106
+ maxRadius: 11, // screenPx
107
107
  minTimeSeconds: 0.5, // s
108
108
  };
109
109
  this.stationaryDetector = new StationaryPenDetector_1.default(current, stationaryDetectionConfig, pointer => this.autocorrectShape(pointer));
@@ -8,6 +8,7 @@ const math_1 = require("@js-draw/math");
8
8
  const types_1 = require("../../types");
9
9
  const Viewport_1 = __importDefault(require("../../Viewport"));
10
10
  const BaseTool_1 = __importDefault(require("../BaseTool"));
11
+ const CanvasRenderer_1 = __importDefault(require("../../rendering/renderers/CanvasRenderer"));
11
12
  const SVGRenderer_1 = __importDefault(require("../../rendering/renderers/SVGRenderer"));
12
13
  const Selection_1 = __importDefault(require("./Selection"));
13
14
  const TextComponent_1 = __importDefault(require("../../components/TextComponent"));
@@ -376,19 +377,37 @@ class SelectionTool extends BaseTool_1.default {
376
377
  return false;
377
378
  }
378
379
  const exportViewport = new Viewport_1.default(() => { });
379
- exportViewport.updateScreenSize(math_1.Vec2.of(bbox.w, bbox.h));
380
- exportViewport.resetTransform(math_1.Mat33.translation(bbox.topLeft.times(-1)));
381
- const sanitize = true;
382
- const { element: svgExportElem, renderer: svgRenderer } = SVGRenderer_1.default.fromViewport(exportViewport, sanitize);
380
+ const selectionScreenSize = this.selectionBox.getScreenRegion().size.times(this.editor.display.getDevicePixelRatio());
381
+ // Update the viewport to have screen size roughly equal to the size of the selection box
382
+ let scaleFactor = selectionScreenSize.maximumEntryMagnitude() / (bbox.size.maximumEntryMagnitude() || 1);
383
+ // Round to a nearby power of two
384
+ scaleFactor = Math.pow(2, Math.ceil(Math.log2(scaleFactor)));
385
+ exportViewport.updateScreenSize(bbox.size.times(scaleFactor));
386
+ exportViewport.resetTransform(math_1.Mat33.scaling2D(scaleFactor)
387
+ // Move the selection onto the screen
388
+ .rightMul(math_1.Mat33.translation(bbox.topLeft.times(-1))));
389
+ const { element: svgExportElem, renderer: svgRenderer } = SVGRenderer_1.default.fromViewport(exportViewport, { sanitize: true, useViewBoxForPositioning: true });
390
+ const { element: canvas, renderer: canvasRenderer } = CanvasRenderer_1.default.fromViewport(exportViewport, { maxCanvasDimen: 4096 });
383
391
  const text = [];
384
392
  for (const elem of selectedElems) {
385
393
  elem.render(svgRenderer);
394
+ elem.render(canvasRenderer);
386
395
  if (elem instanceof TextComponent_1.default) {
387
396
  text.push(elem.getText());
388
397
  }
389
398
  }
390
399
  event.setData('image/svg+xml', svgExportElem.outerHTML);
391
400
  event.setData('text/html', svgExportElem.outerHTML);
401
+ event.setData('image/png', new Promise((resolve, reject) => {
402
+ canvas.toBlob((blob) => {
403
+ if (blob) {
404
+ resolve(blob);
405
+ }
406
+ else {
407
+ reject('Failed to convert canvas to blob.');
408
+ }
409
+ }, 'image/png');
410
+ }));
392
411
  if (text.length > 0) {
393
412
  event.setData('text/plain', text.join('\n'));
394
413
  }
@@ -28,7 +28,7 @@ class ToPointerAutoscroller {
28
28
  return math_1.Vec2.zero;
29
29
  }
30
30
  const closestEdgePoint = autoscrollBoundary.getClosestPointOnBoundaryTo(screenPoint);
31
- const distToEdge = closestEdgePoint.minus(screenPoint).magnitude();
31
+ const distToEdge = closestEdgePoint.distanceTo(screenPoint);
32
32
  const toEdge = closestEdgePoint.minus(screenPoint);
33
33
  // Go faster for points further away from the boundary.
34
34
  const maximumScaleFactor = 1.25;