js-draw 1.9.1 → 1.11.0

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.
Files changed (145) hide show
  1. package/dist/Editor.css +48 -1
  2. package/dist/bundle.js +2 -2
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/Editor.d.ts +41 -0
  5. package/dist/cjs/Editor.js +9 -0
  6. package/dist/cjs/Pointer.js +1 -1
  7. package/dist/cjs/commands/Erase.d.ts +22 -2
  8. package/dist/cjs/commands/Erase.js +22 -2
  9. package/dist/cjs/commands/invertCommand.js +5 -0
  10. package/dist/cjs/commands/uniteCommands.d.ts +36 -0
  11. package/dist/cjs/commands/uniteCommands.js +36 -0
  12. package/dist/cjs/components/AbstractComponent.d.ts +8 -0
  13. package/dist/cjs/components/AbstractComponent.js +28 -8
  14. package/dist/cjs/components/ImageComponent.d.ts +12 -0
  15. package/dist/cjs/components/ImageComponent.js +16 -9
  16. package/dist/cjs/components/Stroke.d.ts +16 -2
  17. package/dist/cjs/components/Stroke.js +17 -1
  18. package/dist/cjs/components/builders/ArrowBuilder.js +3 -3
  19. package/dist/cjs/components/builders/CircleBuilder.js +3 -3
  20. package/dist/cjs/components/builders/FreehandLineBuilder.js +3 -3
  21. package/dist/cjs/components/builders/LineBuilder.js +3 -3
  22. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +3 -3
  23. package/dist/cjs/components/builders/RectangleBuilder.js +5 -6
  24. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.d.ts +3 -0
  25. package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +168 -0
  26. package/dist/cjs/components/builders/autocorrect/makeSnapToGridAutocorrect.d.ts +3 -0
  27. package/dist/cjs/components/builders/autocorrect/makeSnapToGridAutocorrect.js +46 -0
  28. package/dist/cjs/components/builders/types.d.ts +12 -0
  29. package/dist/cjs/image/EditorImage.d.ts +32 -1
  30. package/dist/cjs/image/EditorImage.js +32 -1
  31. package/dist/cjs/rendering/RenderablePathSpec.d.ts +5 -1
  32. package/dist/cjs/rendering/RenderablePathSpec.js +4 -0
  33. package/dist/cjs/toolbar/AbstractToolbar.d.ts +18 -2
  34. package/dist/cjs/toolbar/AbstractToolbar.js +46 -30
  35. package/dist/cjs/toolbar/IconProvider.d.ts +2 -0
  36. package/dist/cjs/toolbar/IconProvider.js +17 -0
  37. package/dist/cjs/toolbar/localization.d.ts +3 -0
  38. package/dist/cjs/toolbar/localization.js +4 -1
  39. package/dist/cjs/toolbar/widgets/BaseWidget.js +1 -1
  40. package/dist/cjs/toolbar/widgets/ExitActionWidget.d.ts +12 -0
  41. package/dist/cjs/toolbar/widgets/ExitActionWidget.js +32 -0
  42. package/dist/cjs/toolbar/widgets/HandToolWidget.d.ts +4 -3
  43. package/dist/cjs/toolbar/widgets/HandToolWidget.js +24 -13
  44. package/dist/cjs/toolbar/widgets/InsertImageWidget.d.ts +2 -1
  45. package/dist/cjs/toolbar/widgets/InsertImageWidget.js +102 -22
  46. package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -2
  47. package/dist/cjs/toolbar/widgets/PenToolWidget.js +50 -20
  48. package/dist/cjs/toolbar/widgets/keybindings.d.ts +1 -0
  49. package/dist/cjs/toolbar/widgets/keybindings.js +4 -1
  50. package/dist/cjs/toolbar/widgets/layout/types.d.ts +1 -1
  51. package/dist/cjs/tools/Pen.d.ts +9 -0
  52. package/dist/cjs/tools/Pen.js +82 -3
  53. package/dist/cjs/tools/SelectionTool/Selection.d.ts +4 -0
  54. package/dist/cjs/tools/SelectionTool/Selection.js +56 -12
  55. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +1 -0
  56. package/dist/cjs/tools/SelectionTool/SelectionTool.js +19 -1
  57. package/dist/cjs/tools/TextTool.js +5 -1
  58. package/dist/cjs/tools/ToolSwitcherShortcut.d.ts +0 -1
  59. package/dist/cjs/tools/ToolSwitcherShortcut.js +0 -1
  60. package/dist/cjs/tools/keybindings.d.ts +1 -0
  61. package/dist/cjs/tools/keybindings.js +3 -1
  62. package/dist/cjs/tools/util/StationaryPenDetector.d.ts +22 -0
  63. package/dist/cjs/tools/util/StationaryPenDetector.js +95 -0
  64. package/dist/cjs/util/ReactiveValue.d.ts +2 -0
  65. package/dist/cjs/util/ReactiveValue.js +2 -0
  66. package/dist/cjs/util/lib.d.ts +1 -0
  67. package/dist/cjs/util/lib.js +4 -1
  68. package/dist/cjs/util/waitForImageLoaded.d.ts +2 -0
  69. package/dist/cjs/util/waitForImageLoaded.js +12 -0
  70. package/dist/cjs/version.js +1 -1
  71. package/dist/mjs/Editor.d.ts +41 -0
  72. package/dist/mjs/Editor.mjs +9 -0
  73. package/dist/mjs/Pointer.mjs +1 -1
  74. package/dist/mjs/commands/Erase.d.ts +22 -2
  75. package/dist/mjs/commands/Erase.mjs +22 -2
  76. package/dist/mjs/commands/invertCommand.mjs +5 -0
  77. package/dist/mjs/commands/uniteCommands.d.ts +36 -0
  78. package/dist/mjs/commands/uniteCommands.mjs +36 -0
  79. package/dist/mjs/components/AbstractComponent.d.ts +8 -0
  80. package/dist/mjs/components/AbstractComponent.mjs +28 -8
  81. package/dist/mjs/components/ImageComponent.d.ts +12 -0
  82. package/dist/mjs/components/ImageComponent.mjs +16 -9
  83. package/dist/mjs/components/Stroke.d.ts +16 -2
  84. package/dist/mjs/components/Stroke.mjs +17 -1
  85. package/dist/mjs/components/builders/ArrowBuilder.mjs +3 -2
  86. package/dist/mjs/components/builders/CircleBuilder.mjs +3 -2
  87. package/dist/mjs/components/builders/FreehandLineBuilder.mjs +3 -2
  88. package/dist/mjs/components/builders/LineBuilder.mjs +3 -2
  89. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +3 -2
  90. package/dist/mjs/components/builders/RectangleBuilder.mjs +5 -4
  91. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.d.ts +3 -0
  92. package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +166 -0
  93. package/dist/mjs/components/builders/autocorrect/makeSnapToGridAutocorrect.d.ts +3 -0
  94. package/dist/mjs/components/builders/autocorrect/makeSnapToGridAutocorrect.mjs +44 -0
  95. package/dist/mjs/components/builders/types.d.ts +12 -0
  96. package/dist/mjs/image/EditorImage.d.ts +32 -1
  97. package/dist/mjs/image/EditorImage.mjs +32 -1
  98. package/dist/mjs/rendering/RenderablePathSpec.d.ts +5 -1
  99. package/dist/mjs/rendering/RenderablePathSpec.mjs +4 -0
  100. package/dist/mjs/toolbar/AbstractToolbar.d.ts +18 -2
  101. package/dist/mjs/toolbar/AbstractToolbar.mjs +46 -30
  102. package/dist/mjs/toolbar/IconProvider.d.ts +2 -0
  103. package/dist/mjs/toolbar/IconProvider.mjs +17 -0
  104. package/dist/mjs/toolbar/localization.d.ts +3 -0
  105. package/dist/mjs/toolbar/localization.mjs +4 -1
  106. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +1 -1
  107. package/dist/mjs/toolbar/widgets/ExitActionWidget.d.ts +12 -0
  108. package/dist/mjs/toolbar/widgets/ExitActionWidget.mjs +27 -0
  109. package/dist/mjs/toolbar/widgets/HandToolWidget.d.ts +4 -3
  110. package/dist/mjs/toolbar/widgets/HandToolWidget.mjs +24 -13
  111. package/dist/mjs/toolbar/widgets/InsertImageWidget.d.ts +2 -1
  112. package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +102 -22
  113. package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -2
  114. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +50 -20
  115. package/dist/mjs/toolbar/widgets/keybindings.d.ts +1 -0
  116. package/dist/mjs/toolbar/widgets/keybindings.mjs +3 -0
  117. package/dist/mjs/toolbar/widgets/layout/types.d.ts +1 -1
  118. package/dist/mjs/tools/Pen.d.ts +9 -0
  119. package/dist/mjs/tools/Pen.mjs +82 -3
  120. package/dist/mjs/tools/SelectionTool/Selection.d.ts +4 -0
  121. package/dist/mjs/tools/SelectionTool/Selection.mjs +56 -12
  122. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +1 -0
  123. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +20 -2
  124. package/dist/mjs/tools/TextTool.mjs +5 -1
  125. package/dist/mjs/tools/ToolSwitcherShortcut.d.ts +0 -1
  126. package/dist/mjs/tools/ToolSwitcherShortcut.mjs +0 -1
  127. package/dist/mjs/tools/keybindings.d.ts +1 -0
  128. package/dist/mjs/tools/keybindings.mjs +2 -0
  129. package/dist/mjs/tools/util/StationaryPenDetector.d.ts +22 -0
  130. package/dist/mjs/tools/util/StationaryPenDetector.mjs +92 -0
  131. package/dist/mjs/util/ReactiveValue.d.ts +2 -0
  132. package/dist/mjs/util/ReactiveValue.mjs +2 -0
  133. package/dist/mjs/util/lib.d.ts +1 -0
  134. package/dist/mjs/util/lib.mjs +1 -0
  135. package/dist/mjs/util/waitForImageLoaded.d.ts +2 -0
  136. package/dist/mjs/util/waitForImageLoaded.mjs +10 -0
  137. package/dist/mjs/version.mjs +1 -1
  138. package/package.json +3 -3
  139. package/src/Editor.scss +7 -0
  140. package/src/toolbar/AbstractToolbar.scss +20 -0
  141. package/src/toolbar/toolbar.scss +1 -1
  142. package/src/toolbar/widgets/InsertImageWidget.scss +6 -1
  143. package/src/toolbar/widgets/PenToolWidget.scss +33 -0
  144. package/src/tools/SelectionTool/SelectionTool.scss +6 -0
  145. package/src/toolbar/widgets/PenToolWidget.css +0 -2
@@ -18,6 +18,9 @@ import { pathFromRenderable, pathToRenderable, simplifyPathToFullScreenOrEmpty
18
18
  * ```ts
19
19
  * editor.dispatch(stroke.transformBy(Mat33.translation(Vec2.of(10, 0))));
20
20
  * ```
21
+ *
22
+ * **Adding**:
23
+ * [[include:doc-pages/inline-examples/adding-a-stroke.md]]
21
24
  */
22
25
  export default class Stroke extends AbstractComponent {
23
26
  /**
@@ -32,7 +35,7 @@ export default class Stroke extends AbstractComponent {
32
35
  *
33
36
  * const stroke = new Stroke([
34
37
  * // Fill with red
35
- * pathToRenderable({ fill: Color4.red })
38
+ * pathToRenderable(path, { fill: Color4.red })
36
39
  * ]);
37
40
  * ```
38
41
  */
@@ -290,6 +293,19 @@ export default class Stroke extends AbstractComponent {
290
293
  };
291
294
  });
292
295
  }
296
+ /**
297
+ * @returns A list of the parts that make up this path. Many paths only have one part.
298
+ *
299
+ * Each part (a {@link RenderablePathSpec}) contains information about the style and geometry
300
+ * of that part of the stroke. Use the `.path` property to do collision detection and other
301
+ * operations involving the stroke's geometry.
302
+ *
303
+ * Note that many of {@link Path}'s methods (e.g. {@link Path.intersection}) take a
304
+ * `strokeWidth` parameter that can be gotten from {@link RenderablePathSpec.style} `.stroke.width`.
305
+ */
306
+ getParts() {
307
+ return [...this.parts];
308
+ }
293
309
  /**
294
310
  * @returns the {@link Path.union} of all paths that make up this stroke.
295
311
  */
@@ -1,8 +1,9 @@
1
1
  import { Path, PathCommandType } from '@js-draw/math';
2
2
  import Stroke from '../Stroke.mjs';
3
- export const makeArrowBuilder = (initialPoint, viewport) => {
3
+ import makeSnapToGridAutocorrect from './autocorrect/makeSnapToGridAutocorrect.mjs';
4
+ export const makeArrowBuilder = makeSnapToGridAutocorrect((initialPoint, viewport) => {
4
5
  return new ArrowBuilder(initialPoint, viewport);
5
- };
6
+ });
6
7
  export default class ArrowBuilder {
7
8
  constructor(startPoint, viewport) {
8
9
  this.startPoint = startPoint;
@@ -2,9 +2,10 @@ import { Vec2, Path, PathCommandType, Color4 } from '@js-draw/math';
2
2
  import { pathToRenderable } from '../../rendering/RenderablePathSpec.mjs';
3
3
  import Viewport from '../../Viewport.mjs';
4
4
  import Stroke from '../Stroke.mjs';
5
- export const makeOutlinedCircleBuilder = (initialPoint, viewport) => {
5
+ import makeSnapToGridAutocorrect from './autocorrect/makeSnapToGridAutocorrect.mjs';
6
+ export const makeOutlinedCircleBuilder = makeSnapToGridAutocorrect((initialPoint, viewport) => {
6
7
  return new CircleBuilder(initialPoint, viewport);
7
- };
8
+ });
8
9
  class CircleBuilder {
9
10
  constructor(startPoint, viewport) {
10
11
  this.startPoint = startPoint;
@@ -2,13 +2,14 @@ import { Vec2, Rect2, Color4, PathCommandType } from '@js-draw/math';
2
2
  import Stroke from '../Stroke.mjs';
3
3
  import Viewport from '../../Viewport.mjs';
4
4
  import { StrokeSmoother } from '../util/StrokeSmoother.mjs';
5
- export const makeFreehandLineBuilder = (initialPoint, viewport) => {
5
+ import makeShapeFitAutocorrect from './autocorrect/makeShapeFitAutocorrect.mjs';
6
+ export const makeFreehandLineBuilder = makeShapeFitAutocorrect((initialPoint, viewport) => {
6
7
  // Don't smooth if input is more than ± 3 pixels from the true curve, do smooth if
7
8
  // less than ±1 px from the curve.
8
9
  const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 3;
9
10
  const minSmoothingDist = viewport.getSizeOfPixelOnCanvas();
10
11
  return new FreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist, viewport);
11
- };
12
+ });
12
13
  // Handles stroke smoothing and creates Strokes from user/stylus input.
13
14
  export default class FreehandLineBuilder {
14
15
  constructor(startPoint, minFitAllowed, maxFitAllowed, viewport) {
@@ -1,9 +1,10 @@
1
1
  import { Path, PathCommandType } from '@js-draw/math';
2
2
  import { pathToRenderable } from '../../rendering/RenderablePathSpec.mjs';
3
3
  import Stroke from '../Stroke.mjs';
4
- export const makeLineBuilder = (initialPoint, viewport) => {
4
+ import makeSnapToGridAutocorrect from './autocorrect/makeSnapToGridAutocorrect.mjs';
5
+ export const makeLineBuilder = makeSnapToGridAutocorrect((initialPoint, viewport) => {
5
6
  return new LineBuilder(initialPoint, viewport);
6
- };
7
+ });
7
8
  export default class LineBuilder {
8
9
  constructor(startPoint, viewport) {
9
10
  this.startPoint = startPoint;
@@ -3,13 +3,14 @@ import { Vec2, Rect2, PathCommandType } from '@js-draw/math';
3
3
  import Stroke from '../Stroke.mjs';
4
4
  import Viewport from '../../Viewport.mjs';
5
5
  import { StrokeSmoother } from '../util/StrokeSmoother.mjs';
6
- export const makePressureSensitiveFreehandLineBuilder = (initialPoint, viewport) => {
6
+ import makeShapeFitAutocorrect from './autocorrect/makeShapeFitAutocorrect.mjs';
7
+ export const makePressureSensitiveFreehandLineBuilder = makeShapeFitAutocorrect((initialPoint, viewport) => {
7
8
  // Don't smooth if input is more than ± 3 pixels from the true curve, do smooth if
8
9
  // less than ±1 px from the curve.
9
10
  const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 3;
10
11
  const minSmoothingDist = viewport.getSizeOfPixelOnCanvas();
11
12
  return new PressureSensitiveFreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist, viewport);
12
- };
13
+ });
13
14
  // Handles stroke smoothing and creates Strokes from user/stylus input.
14
15
  export default class PressureSensitiveFreehandLineBuilder {
15
16
  constructor(startPoint,
@@ -1,12 +1,13 @@
1
1
  import { Mat33, Rect2, Path } from '@js-draw/math';
2
2
  import { pathToRenderable } from '../../rendering/RenderablePathSpec.mjs';
3
3
  import Stroke from '../Stroke.mjs';
4
- export const makeFilledRectangleBuilder = (initialPoint, viewport) => {
4
+ import makeSnapToGridAutocorrect from './autocorrect/makeSnapToGridAutocorrect.mjs';
5
+ export const makeFilledRectangleBuilder = makeSnapToGridAutocorrect((initialPoint, viewport) => {
5
6
  return new RectangleBuilder(initialPoint, true, viewport);
6
- };
7
- export const makeOutlinedRectangleBuilder = (initialPoint, viewport) => {
7
+ });
8
+ export const makeOutlinedRectangleBuilder = makeSnapToGridAutocorrect((initialPoint, viewport) => {
8
9
  return new RectangleBuilder(initialPoint, false, viewport);
9
- };
10
+ });
10
11
  export default class RectangleBuilder {
11
12
  constructor(startPoint, filled, viewport) {
12
13
  this.startPoint = startPoint;
@@ -0,0 +1,3 @@
1
+ import { ComponentBuilderFactory } from '../types';
2
+ declare const makeShapeFitAutocorrect: (sourceFactory: ComponentBuilderFactory) => ComponentBuilderFactory;
3
+ export default makeShapeFitAutocorrect;
@@ -0,0 +1,166 @@
1
+ import { Rect2, LineSegment2 } from '@js-draw/math';
2
+ const makeShapeFitAutocorrect = (sourceFactory) => {
3
+ return (startPoint, viewport) => {
4
+ return new ShapeFitBuilder(sourceFactory, startPoint, viewport);
5
+ };
6
+ };
7
+ export default makeShapeFitAutocorrect;
8
+ const makeLineTemplate = (startPoint, points, _bbox) => {
9
+ const templatePoints = [
10
+ startPoint,
11
+ points[points.length - 1],
12
+ ];
13
+ return { points: templatePoints, };
14
+ };
15
+ const makeRectangleTemplate = (_startPoint, _points, bbox) => {
16
+ return { points: [...bbox.corners, bbox.corners[0]], };
17
+ };
18
+ class ShapeFitBuilder {
19
+ constructor(sourceFactory, startPoint, viewport) {
20
+ this.sourceFactory = sourceFactory;
21
+ this.startPoint = startPoint;
22
+ this.viewport = viewport;
23
+ this.builder = sourceFactory(startPoint, viewport);
24
+ this.points = [startPoint];
25
+ }
26
+ getBBox() {
27
+ return this.builder.getBBox();
28
+ }
29
+ build() {
30
+ return this.builder.build();
31
+ }
32
+ preview(renderer) {
33
+ this.builder.preview(renderer);
34
+ }
35
+ addPoint(point) {
36
+ this.points.push(point);
37
+ this.builder.addPoint(point);
38
+ }
39
+ async autocorrectShape() {
40
+ // Use screen points so that autocorrected shapes rotate with the screen.
41
+ const startPoint = this.viewport.canvasToScreen(this.startPoint.pos);
42
+ const points = this.points.map(point => this.viewport.canvasToScreen(point.pos));
43
+ const bbox = Rect2.bboxOf(points);
44
+ const snappedStartPoint = this.viewport.canvasToScreen(this.viewport.snapToGrid(this.startPoint.pos));
45
+ const snappedPoints = this.points.map(point => this.viewport.canvasToScreen(this.viewport.snapToGrid(point.pos)));
46
+ const snappedBBox = Rect2.bboxOf(snappedPoints);
47
+ // Only fit larger shapes
48
+ if (bbox.maxDimension < 32) {
49
+ return null;
50
+ }
51
+ const maxError = Math.min(30, bbox.maxDimension / 4);
52
+ // Create templates
53
+ const templates = [
54
+ {
55
+ ...makeLineTemplate(snappedStartPoint, snappedPoints, snappedBBox),
56
+ toleranceMultiplier: 0.5,
57
+ },
58
+ makeLineTemplate(startPoint, points, bbox),
59
+ {
60
+ ...makeRectangleTemplate(snappedStartPoint, snappedPoints, snappedBBox),
61
+ toleranceMultiplier: 0.6,
62
+ },
63
+ makeRectangleTemplate(startPoint, points, bbox),
64
+ ];
65
+ // Find a good fit fit
66
+ const selectTemplate = (maximumAllowedError) => {
67
+ for (const template of templates) {
68
+ const templatePoints = template.points;
69
+ // Maximum square error to accept the template
70
+ const acceptMaximumSquareError = maximumAllowedError * maximumAllowedError * (template.toleranceMultiplier ?? 1);
71
+ // Gets the point at index, wrapping the the start of the template if
72
+ // outside the array of points.
73
+ const templateAt = (index) => {
74
+ while (index < 0) {
75
+ index += templatePoints.length;
76
+ }
77
+ index %= templatePoints.length;
78
+ return templatePoints[index];
79
+ };
80
+ let closestToFirst = null;
81
+ let closestToFirstSqrDist = Infinity;
82
+ let templateStartIndex = 0;
83
+ // Find the closest point to the startPoint
84
+ for (let i = 0; i < templatePoints.length; i++) {
85
+ const current = templatePoints[i];
86
+ const currentSqrDist = current.minus(startPoint).magnitudeSquared();
87
+ if (!closestToFirst || currentSqrDist < closestToFirstSqrDist) {
88
+ closestToFirstSqrDist = currentSqrDist;
89
+ closestToFirst = current;
90
+ templateStartIndex = i;
91
+ }
92
+ }
93
+ // Walk through the points and find the maximum error
94
+ let maximumSqrError = 0;
95
+ let templateIndex = templateStartIndex;
96
+ for (const point of points) {
97
+ let minimumCurrentSqrError = Infinity;
98
+ let minimumErrorAtIndex = templateIndex;
99
+ const windowRadius = 6;
100
+ for (let i = -windowRadius; i <= windowRadius; i++) {
101
+ const index = templateIndex + i;
102
+ const prevTemplatePoint = templateAt(index - 1);
103
+ const currentTemplatePoint = templateAt(index);
104
+ const nextTemplatePoint = templateAt(index + 1);
105
+ const prevToCurrent = new LineSegment2(prevTemplatePoint, currentTemplatePoint);
106
+ const currentToNext = new LineSegment2(currentTemplatePoint, nextTemplatePoint);
107
+ const prevToCurrentDist = prevToCurrent.distance(point);
108
+ const nextToCurrentDist = currentToNext.distance(point);
109
+ const error = Math.min(prevToCurrentDist, nextToCurrentDist);
110
+ const squareError = error * error;
111
+ if (squareError < minimumCurrentSqrError) {
112
+ minimumCurrentSqrError = squareError;
113
+ minimumErrorAtIndex = index;
114
+ }
115
+ }
116
+ templateIndex = minimumErrorAtIndex;
117
+ maximumSqrError = Math.max(minimumCurrentSqrError, maximumSqrError);
118
+ if (maximumSqrError > acceptMaximumSquareError) {
119
+ break;
120
+ }
121
+ }
122
+ if (maximumSqrError < acceptMaximumSquareError) {
123
+ return templatePoints;
124
+ }
125
+ }
126
+ return null;
127
+ };
128
+ const template = selectTemplate(maxError);
129
+ if (!template) {
130
+ return null;
131
+ }
132
+ const lastDataPoint = this.points[this.points.length - 1];
133
+ const startWidth = this.startPoint.width;
134
+ const endWidth = lastDataPoint.width;
135
+ const startColor = this.startPoint.color;
136
+ const endColor = lastDataPoint.color;
137
+ const startTime = this.startPoint.time;
138
+ const endTime = lastDataPoint.time;
139
+ const templateIndexToStrokeDataPoint = (index) => {
140
+ const prevPoint = template[Math.max(0, Math.floor(index))];
141
+ const nextPoint = template[Math.min(Math.ceil(index), template.length - 1)];
142
+ const point = prevPoint.lerp(nextPoint, index - Math.floor(index));
143
+ const fractionToEnd = index / template.length;
144
+ return {
145
+ pos: this.viewport.screenToCanvas(point),
146
+ width: startWidth * (1 - fractionToEnd) + endWidth * fractionToEnd,
147
+ color: startColor.mix(endColor, fractionToEnd),
148
+ time: startTime * (1 - fractionToEnd) + endTime * fractionToEnd,
149
+ };
150
+ };
151
+ const builder = this.sourceFactory(templateIndexToStrokeDataPoint(0), this.viewport);
152
+ // Prevent the original builder from doing stroke smoothing if the template is short
153
+ // enough to likely have sharp corners.
154
+ const preventSmoothing = template.length < 10;
155
+ for (let i = 0; i < template.length; i++) {
156
+ if (preventSmoothing) {
157
+ builder.addPoint(templateIndexToStrokeDataPoint(i - 0.001));
158
+ }
159
+ builder.addPoint(templateIndexToStrokeDataPoint(i));
160
+ if (preventSmoothing) {
161
+ builder.addPoint(templateIndexToStrokeDataPoint(i + 0.001));
162
+ }
163
+ }
164
+ return builder.build();
165
+ }
166
+ }
@@ -0,0 +1,3 @@
1
+ import { ComponentBuilderFactory } from '../types';
2
+ declare const makeSnapToGridAutocorrect: (sourceFactory: ComponentBuilderFactory) => ComponentBuilderFactory;
3
+ export default makeSnapToGridAutocorrect;
@@ -0,0 +1,44 @@
1
+ const makeSnapToGridAutocorrect = (sourceFactory) => {
2
+ return (startPoint, viewport) => {
3
+ return new SnapToGridAutocompleteBuilder(sourceFactory, startPoint, viewport);
4
+ };
5
+ };
6
+ export default makeSnapToGridAutocorrect;
7
+ class SnapToGridAutocompleteBuilder {
8
+ constructor(sourceFactory, startPoint, viewport) {
9
+ this.sourceFactory = sourceFactory;
10
+ this.startPoint = startPoint;
11
+ this.viewport = viewport;
12
+ this.builder = sourceFactory(startPoint, viewport);
13
+ this.points = [startPoint];
14
+ }
15
+ getBBox() {
16
+ return this.builder.getBBox();
17
+ }
18
+ build() {
19
+ return this.builder.build();
20
+ }
21
+ preview(renderer) {
22
+ this.builder.preview(renderer);
23
+ }
24
+ addPoint(point) {
25
+ this.points.push(point);
26
+ this.builder.addPoint(point);
27
+ }
28
+ async autocorrectShape() {
29
+ const snapToGrid = (point) => {
30
+ return {
31
+ ...point,
32
+ pos: this.viewport.snapToGrid(point.pos),
33
+ };
34
+ };
35
+ // Use screen points so that snapped shapes rotate with the screen.
36
+ const startPoint = snapToGrid(this.startPoint);
37
+ const builder = this.sourceFactory(startPoint, this.viewport);
38
+ const points = this.points.map(point => snapToGrid(point));
39
+ for (const point of points) {
40
+ builder.addPoint(point);
41
+ }
42
+ return builder.build();
43
+ }
44
+ }
@@ -7,6 +7,18 @@ export interface ComponentBuilder {
7
7
  getBBox(): Rect2;
8
8
  build(): AbstractComponent;
9
9
  preview(renderer: AbstractRenderer): void;
10
+ /**
11
+ * Called when the pen is stationary (or the user otherwise
12
+ * activates autocomplete). This might attempt to fit the user's
13
+ * drawing to a particular shape.
14
+ *
15
+ * The shape returned by this function may be ignored if it has
16
+ * an empty bounding box.
17
+ *
18
+ * Although this returns a Promise, it should return *as fast as
19
+ * possible*.
20
+ */
21
+ autocorrectShape?: () => Promise<AbstractComponent | null>;
10
22
  addPoint(point: StrokeDataPoint): void;
11
23
  }
12
24
  export type ComponentBuilderFactory = (startPoint: StrokeDataPoint, viewport: Viewport) => ComponentBuilder;
@@ -87,6 +87,9 @@ export default class EditorImage {
87
87
  * rendered onto the main rendering canvas instead of doing a full re-render.
88
88
  *
89
89
  * @see {@link Display.flatten}
90
+ *
91
+ * @example
92
+ * [[include:doc-pages/inline-examples/adding-a-stroke.md]]
90
93
  */
91
94
  static addElement(elem: AbstractComponent, applyByFlattening?: boolean): SerializableCommand;
92
95
  /** @see EditorImage.addElement */
@@ -105,8 +108,36 @@ export default class EditorImage {
105
108
  * autoresize (if it was previously enabled).
106
109
  */
107
110
  setImportExportRect(imageRect: Rect2): SerializableCommand;
111
+ /** @see {@link setAutoresizeEnabled} */
108
112
  getAutoresizeEnabled(): boolean;
109
- /** Returns a `Command` that sets whether the image should autoresize. */
113
+ /**
114
+ * Returns a `Command` that sets whether the image should autoresize when
115
+ * {@link AbstractComponent}s are added/removed.
116
+ *
117
+ * @example
118
+ *
119
+ * ```ts,runnable
120
+ * import { Editor } from 'js-draw';
121
+ *
122
+ * const editor = new Editor(document.body);
123
+ * const toolbar = editor.addToolbar();
124
+ *
125
+ * // Add a save button to demonstrate what the output looks like
126
+ * // (it should change size to fit whatever was drawn)
127
+ * toolbar.addSaveButton(() => {
128
+ * document.body.replaceChildren(editor.toSVG({ sanitize: true }));
129
+ * });
130
+ *
131
+ * // Actually using setAutoresizeEnabled:
132
+ * //
133
+ * // To set autoresize without announcing for accessibility/making undoable
134
+ * const addToHistory = false;
135
+ * editor.dispatchNoAnnounce(editor.image.setAutoresizeEnabled(true), addToHistory);
136
+ *
137
+ * // Add to undo history **and** announce for accessibility
138
+ * //editor.dispatch(editor.image.setAutoresizeEnabled(true), true);
139
+ * ```
140
+ */
110
141
  setAutoresizeEnabled(autoresize: boolean): Command;
111
142
  private setAutoresizeEnabledDirectly;
112
143
  /** Updates the size/position of the viewport */
@@ -180,6 +180,9 @@ class EditorImage {
180
180
  * rendered onto the main rendering canvas instead of doing a full re-render.
181
181
  *
182
182
  * @see {@link Display.flatten}
183
+ *
184
+ * @example
185
+ * [[include:doc-pages/inline-examples/adding-a-stroke.md]]
183
186
  */
184
187
  static addElement(elem, applyByFlattening = false) {
185
188
  return new _a.AddElementCommand(elem, applyByFlattening);
@@ -207,10 +210,38 @@ class EditorImage {
207
210
  setImportExportRect(imageRect) {
208
211
  return _a.SetImportExportRectCommand.of(this, imageRect, false);
209
212
  }
213
+ /** @see {@link setAutoresizeEnabled} */
210
214
  getAutoresizeEnabled() {
211
215
  return this.shouldAutoresizeExportViewport;
212
216
  }
213
- /** Returns a `Command` that sets whether the image should autoresize. */
217
+ /**
218
+ * Returns a `Command` that sets whether the image should autoresize when
219
+ * {@link AbstractComponent}s are added/removed.
220
+ *
221
+ * @example
222
+ *
223
+ * ```ts,runnable
224
+ * import { Editor } from 'js-draw';
225
+ *
226
+ * const editor = new Editor(document.body);
227
+ * const toolbar = editor.addToolbar();
228
+ *
229
+ * // Add a save button to demonstrate what the output looks like
230
+ * // (it should change size to fit whatever was drawn)
231
+ * toolbar.addSaveButton(() => {
232
+ * document.body.replaceChildren(editor.toSVG({ sanitize: true }));
233
+ * });
234
+ *
235
+ * // Actually using setAutoresizeEnabled:
236
+ * //
237
+ * // To set autoresize without announcing for accessibility/making undoable
238
+ * const addToHistory = false;
239
+ * editor.dispatchNoAnnounce(editor.image.setAutoresizeEnabled(true), addToHistory);
240
+ *
241
+ * // Add to undo history **and** announce for accessibility
242
+ * //editor.dispatch(editor.image.setAutoresizeEnabled(true), true);
243
+ * ```
244
+ */
214
245
  setAutoresizeEnabled(autoresize) {
215
246
  if (autoresize === this.shouldAutoresizeExportViewport) {
216
247
  return Command.empty;
@@ -6,11 +6,15 @@ interface RenderablePathSpec {
6
6
  style: RenderingStyle;
7
7
  path?: Path;
8
8
  }
9
- interface RenderablePathSpecWithPath extends RenderablePathSpec {
9
+ export interface RenderablePathSpecWithPath extends RenderablePathSpec {
10
10
  path: Path;
11
11
  }
12
12
  /** Converts a renderable path (a path with a `startPoint`, `commands`, and `style`). */
13
13
  export declare const pathFromRenderable: (renderable: RenderablePathSpec) => Path;
14
+ /**
15
+ * Converts `path` into a format that can be rendered (by passing to a {@link Stroke} constructor
16
+ * or directly to an {@link AbstractRenderer.drawPath}).
17
+ */
14
18
  export declare const pathToRenderable: (path: Path, style: RenderingStyle) => RenderablePathSpecWithPath;
15
19
  interface RectangleSimplificationResult {
16
20
  rectangle: Rect2;
@@ -6,6 +6,10 @@ export const pathFromRenderable = (renderable) => {
6
6
  }
7
7
  return new Path(renderable.startPoint, renderable.commands);
8
8
  };
9
+ /**
10
+ * Converts `path` into a format that can be rendered (by passing to a {@link Stroke} constructor
11
+ * or directly to an {@link AbstractRenderer.drawPath}).
12
+ */
9
13
  export const pathToRenderable = (path, style) => {
10
14
  return {
11
15
  startPoint: path.startPoint,
@@ -3,6 +3,7 @@ import { ToolbarLocalization } from './localization';
3
3
  import { ActionButtonIcon } from './types';
4
4
  import BaseWidget, { ToolbarWidgetTag } from './widgets/BaseWidget';
5
5
  import { DispatcherEventListener } from '../EventDispatcher';
6
+ import { BaseTool } from '../lib';
6
7
  export interface SpacerOptions {
7
8
  grow: number;
8
9
  minSize: string;
@@ -131,7 +132,7 @@ export default abstract class AbstractToolbar {
131
132
  /**
132
133
  * Adds an "Exit" button that, when clicked, calls `exitCallback`.
133
134
  *
134
- * **Note**: This is roughly equivalent to
135
+ * **Note**: This is *roughly* equivalent to
135
136
  * ```ts
136
137
  * toolbar.addTaggedActionButton([ ToolbarWidgetTag.Exit ], {
137
138
  * label: this.editor.localization.exit,
@@ -154,9 +155,24 @@ export default abstract class AbstractToolbar {
154
155
  */
155
156
  addUndoRedoButtons(undoFirst?: boolean): void;
156
157
  /**
157
- * Adds toolbar widgets based on the enabled tools.
158
+ * Adds widgets for pen/eraser/selection/text/pan-zoom primary tools.
159
+ *
160
+ * If `filter` returns `false` for a tool, no widget is added for that tool.
161
+ * See {@link addDefaultToolWidgets}
162
+ */
163
+ addWidgetsForPrimaryTools(filter?: (tool: BaseTool) => boolean): void;
164
+ /**
165
+ * Adds toolbar widgets based on the enabled tools, and additional tool-like
166
+ * buttons (e.g. {@link DocumentPropertiesWidget} and {@link InsertImageWidget}).
158
167
  */
159
168
  addDefaultToolWidgets(): void;
169
+ /**
170
+ * Adds widgets that don't correspond to tools, but do allow the user to control
171
+ * the editor in some way.
172
+ *
173
+ * By default, this includes {@link DocumentPropertiesWidget} and {@link InsertImageWidget}.
174
+ */
175
+ addDefaultEditorControlWidgets(): void;
160
176
  addDefaultActionButtons(): void;
161
177
  /**
162
178
  * Adds both the default tool widgets and action buttons.
@@ -30,6 +30,7 @@ import DocumentPropertiesWidget from './widgets/DocumentPropertiesWidget.mjs';
30
30
  import { Color4 } from '@js-draw/math';
31
31
  import { toolbarCSSPrefix } from './constants.mjs';
32
32
  import SaveActionWidget from './widgets/SaveActionWidget.mjs';
33
+ import ExitActionWidget from './widgets/ExitActionWidget.mjs';
33
34
  class AbstractToolbar {
34
35
  /** @internal */
35
36
  constructor(editor, localizationTable = defaultToolbarLocalization) {
@@ -292,14 +293,13 @@ class AbstractToolbar {
292
293
  */
293
294
  addSaveButton(saveCallback, labelOverride = {}) {
294
295
  const widget = new SaveActionWidget(this.editor, this.localizationTable, saveCallback, labelOverride);
295
- widget.setTags([ToolbarWidgetTag.Save]);
296
296
  this.addWidget(widget);
297
297
  return widget;
298
298
  }
299
299
  /**
300
300
  * Adds an "Exit" button that, when clicked, calls `exitCallback`.
301
301
  *
302
- * **Note**: This is roughly equivalent to
302
+ * **Note**: This is *roughly* equivalent to
303
303
  * ```ts
304
304
  * toolbar.addTaggedActionButton([ ToolbarWidgetTag.Exit ], {
305
305
  * label: this.editor.localization.exit,
@@ -316,15 +316,9 @@ class AbstractToolbar {
316
316
  * @final
317
317
  */
318
318
  addExitButton(exitCallback, labelOverride = {}) {
319
- return this.addTaggedActionButton([ToolbarWidgetTag.Exit], {
320
- label: this.editor.localization.exit,
321
- icon: this.editor.icons.makeCloseIcon(),
322
- ...labelOverride,
323
- }, () => {
324
- exitCallback();
325
- }, {
326
- autoDisableInReadOnlyEditors: false,
327
- });
319
+ const widget = new ExitActionWidget(this.editor, this.localizationTable, exitCallback, labelOverride);
320
+ this.addWidget(widget);
321
+ return widget;
328
322
  }
329
323
  /**
330
324
  * Adds undo and redo buttons that trigger the editor's built-in undo and redo
@@ -372,27 +366,49 @@ class AbstractToolbar {
372
366
  });
373
367
  }
374
368
  /**
375
- * Adds toolbar widgets based on the enabled tools.
369
+ * Adds widgets for pen/eraser/selection/text/pan-zoom primary tools.
370
+ *
371
+ * If `filter` returns `false` for a tool, no widget is added for that tool.
372
+ * See {@link addDefaultToolWidgets}
376
373
  */
377
- addDefaultToolWidgets() {
378
- const toolController = this.editor.toolController;
379
- for (const tool of toolController.getMatchingTools(PenTool)) {
380
- const widget = new PenToolWidget(this.editor, tool, this.localizationTable);
381
- this.addWidget(widget);
382
- }
383
- for (const tool of toolController.getMatchingTools(EraserTool)) {
384
- this.addWidget(new EraserWidget(this.editor, tool, this.localizationTable));
385
- }
386
- for (const tool of toolController.getMatchingTools(SelectionTool)) {
387
- this.addWidget(new SelectionToolWidget(this.editor, tool, this.localizationTable));
388
- }
389
- for (const tool of toolController.getMatchingTools(TextTool)) {
390
- this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
391
- }
392
- const panZoomTool = toolController.getMatchingTools(PanZoomTool)[0];
393
- if (panZoomTool) {
394
- this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));
374
+ addWidgetsForPrimaryTools(filter) {
375
+ for (const tool of this.editor.toolController.getPrimaryTools()) {
376
+ if (filter && !filter?.(tool)) {
377
+ continue;
378
+ }
379
+ if (tool instanceof PenTool) {
380
+ const widget = new PenToolWidget(this.editor, tool, this.localizationTable);
381
+ this.addWidget(widget);
382
+ }
383
+ else if (tool instanceof EraserTool) {
384
+ this.addWidget(new EraserWidget(this.editor, tool, this.localizationTable));
385
+ }
386
+ else if (tool instanceof SelectionTool) {
387
+ this.addWidget(new SelectionToolWidget(this.editor, tool, this.localizationTable));
388
+ }
389
+ else if (tool instanceof TextTool) {
390
+ this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
391
+ }
392
+ else if (tool instanceof PanZoomTool) {
393
+ this.addWidget(new HandToolWidget(this.editor, tool, this.localizationTable));
394
+ }
395
395
  }
396
+ }
397
+ /**
398
+ * Adds toolbar widgets based on the enabled tools, and additional tool-like
399
+ * buttons (e.g. {@link DocumentPropertiesWidget} and {@link InsertImageWidget}).
400
+ */
401
+ addDefaultToolWidgets() {
402
+ this.addWidgetsForPrimaryTools();
403
+ this.addDefaultEditorControlWidgets();
404
+ }
405
+ /**
406
+ * Adds widgets that don't correspond to tools, but do allow the user to control
407
+ * the editor in some way.
408
+ *
409
+ * By default, this includes {@link DocumentPropertiesWidget} and {@link InsertImageWidget}.
410
+ */
411
+ addDefaultEditorControlWidgets() {
396
412
  this.addWidget(new DocumentPropertiesWidget(this.editor, this.localizationTable));
397
413
  this.addWidget(new InsertImageWidget(this.editor, this.localizationTable));
398
414
  }