js-draw 1.17.0 → 1.19.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (181) hide show
  1. package/README.md +70 -10
  2. package/dist/Editor.css +35 -3
  3. package/dist/bundle.js +2 -2
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/Editor.d.ts +38 -21
  6. package/dist/cjs/Editor.js +11 -2
  7. package/dist/cjs/{SVGLoader.d.ts → SVGLoader/index.d.ts} +1 -1
  8. package/dist/cjs/{SVGLoader.js → SVGLoader/index.js} +12 -29
  9. package/dist/cjs/SVGLoader/utils/determineFontSize.d.ts +3 -0
  10. package/dist/cjs/SVGLoader/utils/determineFontSize.js +27 -0
  11. package/dist/cjs/Viewport.d.ts +33 -1
  12. package/dist/cjs/components/AbstractComponent.d.ts +17 -5
  13. package/dist/cjs/components/AbstractComponent.js +15 -15
  14. package/dist/cjs/components/Stroke.d.ts +4 -1
  15. package/dist/cjs/components/Stroke.js +158 -2
  16. package/dist/cjs/components/TextComponent.js +3 -1
  17. package/dist/cjs/components/builders/PolylineBuilder.d.ts +1 -1
  18. package/dist/cjs/components/builders/PolylineBuilder.js +9 -2
  19. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
  20. package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +44 -51
  21. package/dist/cjs/image/EditorImage.js +1 -1
  22. package/dist/cjs/localizations/de.js +1 -1
  23. package/dist/cjs/localizations/es.js +1 -1
  24. package/dist/cjs/rendering/caching/RenderingCacheNode.js +20 -15
  25. package/dist/cjs/testing/createEditor.d.ts +2 -2
  26. package/dist/cjs/testing/createEditor.js +2 -2
  27. package/dist/cjs/testing/findNodeWithText.d.ts +3 -0
  28. package/dist/cjs/testing/findNodeWithText.js +16 -0
  29. package/dist/cjs/testing/firstElementAncestorOfNode.d.ts +3 -0
  30. package/dist/cjs/testing/firstElementAncestorOfNode.js +13 -0
  31. package/dist/cjs/testing/sendKeyPressRelease.d.ts +3 -0
  32. package/dist/cjs/testing/sendKeyPressRelease.js +8 -0
  33. package/dist/cjs/testing/sendPenEvent.d.ts +2 -2
  34. package/dist/cjs/testing/sendPenEvent.js +26 -3
  35. package/dist/cjs/toolbar/IconProvider.d.ts +3 -1
  36. package/dist/cjs/toolbar/IconProvider.js +15 -3
  37. package/dist/cjs/toolbar/localization.d.ts +8 -1
  38. package/dist/cjs/toolbar/localization.js +9 -2
  39. package/dist/cjs/toolbar/widgets/BaseWidget.d.ts +1 -0
  40. package/dist/cjs/toolbar/widgets/BaseWidget.js +1 -0
  41. package/dist/cjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
  42. package/dist/cjs/toolbar/widgets/EraserToolWidget.js +45 -5
  43. package/dist/cjs/toolbar/widgets/InsertImageWidget/ImageWrapper.d.ts +22 -0
  44. package/dist/cjs/toolbar/widgets/InsertImageWidget/ImageWrapper.js +58 -0
  45. package/dist/cjs/toolbar/widgets/InsertImageWidget/fileToImages.d.ts +3 -0
  46. package/dist/cjs/toolbar/widgets/InsertImageWidget/fileToImages.js +21 -0
  47. package/dist/cjs/toolbar/widgets/InsertImageWidget/index.d.ts +37 -0
  48. package/dist/cjs/toolbar/widgets/InsertImageWidget/index.js +281 -0
  49. package/dist/cjs/toolbar/widgets/PenToolWidget.js +10 -3
  50. package/dist/cjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
  51. package/dist/cjs/toolbar/widgets/TextToolWidget.js +5 -3
  52. package/dist/cjs/toolbar/widgets/TextToolWidget.test.d.ts +1 -0
  53. package/dist/cjs/toolbar/widgets/components/makeFileInput.d.ts +12 -2
  54. package/dist/cjs/toolbar/widgets/components/makeFileInput.js +102 -45
  55. package/dist/cjs/toolbar/widgets/components/makeFileInput.test.d.ts +1 -0
  56. package/dist/cjs/toolbar/widgets/components/makeSnappedList.d.ts +15 -0
  57. package/dist/cjs/toolbar/widgets/components/makeSnappedList.js +103 -0
  58. package/dist/cjs/toolbar/widgets/keybindings.js +1 -1
  59. package/dist/cjs/tools/Eraser.d.ts +31 -6
  60. package/dist/cjs/tools/Eraser.js +161 -21
  61. package/dist/cjs/tools/PasteHandler.js +0 -1
  62. package/dist/cjs/tools/SelectionTool/Selection.d.ts +2 -2
  63. package/dist/cjs/tools/SelectionTool/Selection.js +20 -20
  64. package/dist/cjs/tools/SelectionTool/SelectionHandle.d.ts +8 -2
  65. package/dist/cjs/tools/SelectionTool/SelectionHandle.js +6 -0
  66. package/dist/cjs/tools/SelectionTool/SelectionTool.js +1 -1
  67. package/dist/cjs/tools/SelectionTool/types.d.ts +19 -0
  68. package/dist/cjs/tools/TextTool.js +2 -1
  69. package/dist/cjs/tools/TextTool.test.d.ts +1 -0
  70. package/dist/cjs/tools/ToolController.d.ts +2 -0
  71. package/dist/cjs/tools/ToolController.js +10 -1
  72. package/dist/cjs/tools/lib.d.ts +1 -4
  73. package/dist/cjs/tools/lib.js +2 -4
  74. package/dist/cjs/util/ReactiveValue.d.ts +2 -0
  75. package/dist/cjs/util/ReactiveValue.js +11 -0
  76. package/dist/cjs/util/bytesToSizeString.d.ts +8 -0
  77. package/dist/cjs/util/bytesToSizeString.js +26 -0
  78. package/dist/cjs/util/bytesToSizeString.test.d.ts +1 -0
  79. package/dist/cjs/util/stopPropagationOfScrollingWheelEvents.js +10 -6
  80. package/dist/cjs/util/waitForAll.d.ts +2 -0
  81. package/dist/cjs/util/waitForAll.js +2 -0
  82. package/dist/cjs/util/waitForImageLoaded.js +3 -0
  83. package/dist/cjs/util/waitForTimeout.d.ts +1 -0
  84. package/dist/cjs/util/waitForTimeout.js +1 -1
  85. package/dist/cjs/version.js +1 -1
  86. package/dist/mjs/Editor.d.ts +38 -21
  87. package/dist/mjs/Editor.mjs +11 -2
  88. package/dist/mjs/{SVGLoader.d.ts → SVGLoader/index.d.ts} +1 -1
  89. package/dist/mjs/{SVGLoader.mjs → SVGLoader/index.mjs} +12 -29
  90. package/dist/mjs/SVGLoader/index.test.d.ts +1 -0
  91. package/dist/mjs/SVGLoader/utils/determineFontSize.d.ts +3 -0
  92. package/dist/mjs/SVGLoader/utils/determineFontSize.mjs +25 -0
  93. package/dist/mjs/Viewport.d.ts +33 -1
  94. package/dist/mjs/components/AbstractComponent.d.ts +17 -5
  95. package/dist/mjs/components/AbstractComponent.mjs +15 -15
  96. package/dist/mjs/components/Stroke.d.ts +4 -1
  97. package/dist/mjs/components/Stroke.mjs +159 -3
  98. package/dist/mjs/components/TextComponent.mjs +3 -1
  99. package/dist/mjs/components/builders/PolylineBuilder.d.ts +1 -1
  100. package/dist/mjs/components/builders/PolylineBuilder.mjs +10 -3
  101. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
  102. package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +45 -52
  103. package/dist/mjs/image/EditorImage.mjs +1 -1
  104. package/dist/mjs/localizations/de.mjs +1 -1
  105. package/dist/mjs/localizations/es.mjs +1 -1
  106. package/dist/mjs/rendering/caching/RenderingCacheNode.mjs +20 -15
  107. package/dist/mjs/testing/createEditor.d.ts +2 -2
  108. package/dist/mjs/testing/createEditor.mjs +2 -2
  109. package/dist/mjs/testing/findNodeWithText.d.ts +3 -0
  110. package/dist/mjs/testing/findNodeWithText.mjs +14 -0
  111. package/dist/mjs/testing/firstElementAncestorOfNode.d.ts +3 -0
  112. package/dist/mjs/testing/firstElementAncestorOfNode.mjs +11 -0
  113. package/dist/mjs/testing/sendKeyPressRelease.d.ts +3 -0
  114. package/dist/mjs/testing/sendKeyPressRelease.mjs +6 -0
  115. package/dist/mjs/testing/sendPenEvent.d.ts +2 -2
  116. package/dist/mjs/testing/sendPenEvent.mjs +3 -3
  117. package/dist/mjs/toolbar/IconProvider.d.ts +3 -1
  118. package/dist/mjs/toolbar/IconProvider.mjs +15 -3
  119. package/dist/mjs/toolbar/localization.d.ts +8 -1
  120. package/dist/mjs/toolbar/localization.mjs +9 -2
  121. package/dist/mjs/toolbar/widgets/BaseWidget.d.ts +1 -0
  122. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +1 -0
  123. package/dist/mjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
  124. package/dist/mjs/toolbar/widgets/EraserToolWidget.mjs +47 -6
  125. package/dist/mjs/toolbar/widgets/InsertImageWidget/ImageWrapper.d.ts +22 -0
  126. package/dist/mjs/toolbar/widgets/InsertImageWidget/ImageWrapper.mjs +54 -0
  127. package/dist/mjs/toolbar/widgets/InsertImageWidget/fileToImages.d.ts +3 -0
  128. package/dist/mjs/toolbar/widgets/InsertImageWidget/fileToImages.mjs +16 -0
  129. package/dist/mjs/toolbar/widgets/InsertImageWidget/index.d.ts +37 -0
  130. package/dist/mjs/toolbar/widgets/InsertImageWidget/index.mjs +276 -0
  131. package/dist/mjs/toolbar/widgets/InsertImageWidget/index.test.d.ts +1 -0
  132. package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +10 -3
  133. package/dist/mjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
  134. package/dist/mjs/toolbar/widgets/TextToolWidget.mjs +5 -3
  135. package/dist/mjs/toolbar/widgets/TextToolWidget.test.d.ts +1 -0
  136. package/dist/mjs/toolbar/widgets/components/makeFileInput.d.ts +12 -2
  137. package/dist/mjs/toolbar/widgets/components/makeFileInput.mjs +102 -45
  138. package/dist/mjs/toolbar/widgets/components/makeFileInput.test.d.ts +1 -0
  139. package/dist/mjs/toolbar/widgets/components/makeSnappedList.d.ts +15 -0
  140. package/dist/mjs/toolbar/widgets/components/makeSnappedList.mjs +98 -0
  141. package/dist/mjs/toolbar/widgets/keybindings.mjs +1 -1
  142. package/dist/mjs/tools/Eraser.d.ts +31 -6
  143. package/dist/mjs/tools/Eraser.mjs +161 -22
  144. package/dist/mjs/tools/PasteHandler.mjs +0 -1
  145. package/dist/mjs/tools/SelectionTool/Selection.d.ts +2 -2
  146. package/dist/mjs/tools/SelectionTool/Selection.mjs +20 -20
  147. package/dist/mjs/tools/SelectionTool/SelectionHandle.d.ts +8 -2
  148. package/dist/mjs/tools/SelectionTool/SelectionHandle.mjs +6 -0
  149. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +1 -1
  150. package/dist/mjs/tools/SelectionTool/types.d.ts +19 -0
  151. package/dist/mjs/tools/TextTool.mjs +2 -1
  152. package/dist/mjs/tools/TextTool.test.d.ts +1 -0
  153. package/dist/mjs/tools/ToolController.d.ts +2 -0
  154. package/dist/mjs/tools/ToolController.mjs +10 -1
  155. package/dist/mjs/tools/lib.d.ts +1 -4
  156. package/dist/mjs/tools/lib.mjs +1 -4
  157. package/dist/mjs/util/ReactiveValue.d.ts +2 -0
  158. package/dist/mjs/util/ReactiveValue.mjs +11 -0
  159. package/dist/mjs/util/bytesToSizeString.d.ts +8 -0
  160. package/dist/mjs/util/bytesToSizeString.mjs +24 -0
  161. package/dist/mjs/util/bytesToSizeString.test.d.ts +1 -0
  162. package/dist/mjs/util/stopPropagationOfScrollingWheelEvents.mjs +10 -6
  163. package/dist/mjs/util/waitForAll.d.ts +2 -0
  164. package/dist/mjs/util/waitForAll.mjs +2 -0
  165. package/dist/mjs/util/waitForImageLoaded.mjs +3 -0
  166. package/dist/mjs/util/waitForTimeout.d.ts +1 -0
  167. package/dist/mjs/util/waitForTimeout.mjs +1 -1
  168. package/dist/mjs/version.mjs +1 -1
  169. package/package.json +4 -4
  170. package/src/toolbar/toolbar.scss +1 -7
  171. package/src/toolbar/widgets/{InsertImageWidget.scss → InsertImageWidget/index.scss} +3 -2
  172. package/src/toolbar/widgets/components/components.scss +2 -1
  173. package/src/toolbar/widgets/components/makeFileInput.scss +14 -1
  174. package/src/toolbar/widgets/components/makeSnappedList.scss +28 -0
  175. package/src/toolbar/widgets/widgets.scss +7 -0
  176. package/dist/cjs/toolbar/widgets/InsertImageWidget.d.ts +0 -22
  177. package/dist/cjs/toolbar/widgets/InsertImageWidget.js +0 -269
  178. package/dist/mjs/toolbar/widgets/InsertImageWidget.d.ts +0 -22
  179. package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +0 -264
  180. /package/dist/cjs/{SVGLoader.test.d.ts → SVGLoader/index.test.d.ts} +0 -0
  181. /package/dist/{mjs/SVGLoader.test.d.ts → cjs/toolbar/widgets/InsertImageWidget/index.test.d.ts} +0 -0
@@ -6,6 +6,7 @@ import AbstractComponent from './AbstractComponent';
6
6
  import { ImageComponentLocalization } from './localization';
7
7
  import RestyleableComponent, { ComponentStyle } from './RestylableComponent';
8
8
  import RenderablePathSpec, { RenderablePathSpecWithPath } from '../rendering/RenderablePathSpec';
9
+ import Viewport from '../Viewport';
9
10
  /**
10
11
  * Represents an {@link AbstractComponent} made up of one or more {@link Path}s.
11
12
  *
@@ -46,10 +47,12 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
46
47
  * ]);
47
48
  * ```
48
49
  */
49
- constructor(parts: RenderablePathSpec[]);
50
+ constructor(parts: RenderablePathSpec[], initialZIndex?: number);
50
51
  getStyle(): ComponentStyle;
51
52
  updateStyle(style: ComponentStyle): SerializableCommand;
52
53
  forceStyle(style: ComponentStyle, editor: Editor | null): void;
54
+ /** @beta -- May fail for concave `path`s */
55
+ withRegionErased(eraserPath: Path, viewport: Viewport): Stroke[];
53
56
  intersects(line: LineSegment2): boolean;
54
57
  intersectsRect(rect: Rect2): boolean;
55
58
  private simplifiedPath;
@@ -1,4 +1,4 @@
1
- import { Path, Rect2 } from '@js-draw/math';
1
+ import { Path, Rect2, PathCommandType, comparePathIndices, stepPathIndexBy } from '@js-draw/math';
2
2
  import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle.mjs';
3
3
  import AbstractComponent from './AbstractComponent.mjs';
4
4
  import { createRestyleComponentCommand } from './RestylableComponent.mjs';
@@ -39,8 +39,8 @@ export default class Stroke extends AbstractComponent {
39
39
  * ]);
40
40
  * ```
41
41
  */
42
- constructor(parts) {
43
- super('stroke');
42
+ constructor(parts, initialZIndex) {
43
+ super('stroke', initialZIndex);
44
44
  // @internal
45
45
  // eslint-disable-next-line @typescript-eslint/prefer-as-const
46
46
  this.isRestylableComponent = true;
@@ -118,6 +118,162 @@ export default class Stroke extends AbstractComponent {
118
118
  editor.queueRerender();
119
119
  }
120
120
  }
121
+ /** @beta -- May fail for concave `path`s */
122
+ withRegionErased(eraserPath, viewport) {
123
+ const polyline = eraserPath.polylineApproximation();
124
+ const isPointInsideEraser = (point) => {
125
+ return eraserPath.closedContainsPoint(point);
126
+ };
127
+ const newStrokes = [];
128
+ let failedAssertions = false;
129
+ for (const part of this.parts) {
130
+ const path = part.path;
131
+ const makeStroke = (path) => {
132
+ if (part.style.fill.a > 0) {
133
+ // Remove visually empty paths.
134
+ if (path.parts.length < 1 || (path.parts.length === 1 && path.parts[0].kind === PathCommandType.LineTo)) {
135
+ // TODO: If this isn't present, a very large number of strokes are created while erasing.
136
+ return null;
137
+ }
138
+ else {
139
+ // Filled paths must be closed (allows for optimizations elsewhere)
140
+ path = path.asClosed();
141
+ }
142
+ }
143
+ if (isNaN(path.getExactBBox().area)) {
144
+ console.warn('Prevented creating a stroke with NaN area');
145
+ failedAssertions = true;
146
+ return null;
147
+ }
148
+ return new Stroke([pathToRenderable(path, part.style)], this.getZIndex());
149
+ };
150
+ const intersectionPoints = [];
151
+ // If stroked, finds intersections with the middle of the stroke.
152
+ // If filled, finds intersections with the edge of the stroke.
153
+ for (const segment of polyline) {
154
+ intersectionPoints.push(...path.intersection(segment));
155
+ }
156
+ // When stroked, if the stroke width is significantly larger than the eraser,
157
+ // it can't intersect both the edge of the stroke and its middle at the same time
158
+ // (generally, erasing is triggered by the eraser touching the edge of this stroke).
159
+ //
160
+ // As such, we also look for intersections along the edge of this, if none with the
161
+ // center were found, but only within a certain range of sizes because:
162
+ // 1. Intersection testing with stroked paths is generally much slower than with
163
+ // non-stroked paths.
164
+ // 2. If zoomed in significantly, it's unlikely that the user wants to erase a large
165
+ // part of the stroke.
166
+ let isErasingFromEdge = false;
167
+ if (intersectionPoints.length === 0
168
+ && part.style.stroke
169
+ && part.style.stroke.width > eraserPath.bbox.minDimension * 0.3
170
+ && part.style.stroke.width < eraserPath.bbox.maxDimension * 30) {
171
+ for (const segment of polyline) {
172
+ intersectionPoints.push(...path.intersection(segment, part.style.stroke.width / 2));
173
+ }
174
+ isErasingFromEdge = true;
175
+ }
176
+ // Sort first by curve index, then by parameter value
177
+ intersectionPoints.sort(comparePathIndices);
178
+ const isInsideJustBeforeFirst = (() => {
179
+ if (intersectionPoints.length === 0) {
180
+ return false;
181
+ }
182
+ // The eraser may not be near the center of the curve -- approximate.
183
+ if (isErasingFromEdge) {
184
+ return intersectionPoints[0].curveIndex === 0 && intersectionPoints[0].parameterValue <= 0;
185
+ }
186
+ const justBeforeFirstIntersection = stepPathIndexBy(intersectionPoints[0], -1e-10);
187
+ return isPointInsideEraser(path.at(justBeforeFirstIntersection));
188
+ })();
189
+ let intersectionCount = isInsideJustBeforeFirst ? 1 : 0;
190
+ const addNewPath = (path, knownToBeInside) => {
191
+ const component = makeStroke(path);
192
+ let isInside = intersectionCount % 2 === 1;
193
+ intersectionCount++;
194
+ if (knownToBeInside !== undefined) {
195
+ isInside = knownToBeInside;
196
+ }
197
+ // Here, we work around bugs in the underlying Bezier curve library
198
+ // (including https://github.com/Pomax/bezierjs/issues/179).
199
+ // Even if not all intersections are returned correctly, we still want
200
+ // isInside to be roughly correct.
201
+ if (knownToBeInside === undefined && !isInside && eraserPath.closedContainsPoint(path.getExactBBox().center)) {
202
+ isInside = !isInside;
203
+ }
204
+ if (!component) {
205
+ return;
206
+ }
207
+ // Assertion: Avoid deleting sections that are much larger than the eraser.
208
+ failedAssertions ||= isInside && path.getExactBBox().maxDimension > eraserPath.getExactBBox().maxDimension * 2;
209
+ if (!isInside) {
210
+ newStrokes.push(component);
211
+ }
212
+ };
213
+ if (part.style.fill.a === 0) { // Not filled?
214
+ // An additional case where we erase completely -- without the padding of the stroke,
215
+ // the path is smaller than the eraser (allows us to erase dots completely).
216
+ const shouldEraseCompletely = eraserPath.getExactBBox().maxDimension / 10 > path.getExactBBox().maxDimension;
217
+ if (!shouldEraseCompletely) {
218
+ const split = path.splitAt(intersectionPoints, { mapNewPoint: p => viewport.roundPoint(p) });
219
+ for (const splitPart of split) {
220
+ addNewPath(splitPart);
221
+ }
222
+ }
223
+ }
224
+ else if (intersectionPoints.length >= 2 && intersectionPoints.length % 2 === 0) {
225
+ // TODO: Support subtractive erasing on small scales -- see https://github.com/personalizedrefrigerator/js-draw/pull/63/commits/568686e2384219ad0bb07617ea4efff1540aed00
226
+ // for a broken implementation.
227
+ //
228
+ // We currently assume that a 4-point intersection means that the intersection
229
+ // looks similar to this:
230
+ // -----------
231
+ // | STROKE |
232
+ // | |
233
+ //%%x-----------x%%%%%%%
234
+ //% %
235
+ //% ERASER %
236
+ //% %
237
+ //%%x-----------x%%%%%%%
238
+ // | STROKE |
239
+ // -----------
240
+ //
241
+ // Our goal is to separate STROKE into the contiguous parts outside
242
+ // of the eraser (as shown above).
243
+ //
244
+ // To do this, we split STROKE at each intersection:
245
+ // 3 3 3 3 3 3
246
+ // 3 STROKE 3
247
+ // 3 3
248
+ // x x
249
+ // 2 4
250
+ // 2 STROKE 4
251
+ // 2 4
252
+ // x x
253
+ // 1 STROKE 5
254
+ // . 5 5 5 5 5
255
+ // ^
256
+ // Start
257
+ //
258
+ // The difficulty here is correctly pairing edges to create the the output
259
+ // strokes, particularly because we don't know the order of intersection points.
260
+ const parts = path.splitAt(intersectionPoints, { mapNewPoint: p => viewport.roundPoint(p) });
261
+ for (let i = 0; i < Math.floor(parts.length / 2); i++) {
262
+ addNewPath(parts[i].union(parts[parts.length - i - 1]).asClosed());
263
+ }
264
+ if (parts.length % 2 !== 0) {
265
+ addNewPath(parts[Math.floor(parts.length / 2)].asClosed());
266
+ }
267
+ }
268
+ else {
269
+ addNewPath(path, false);
270
+ }
271
+ }
272
+ if (failedAssertions) {
273
+ return [this];
274
+ }
275
+ return newStrokes;
276
+ }
121
277
  intersects(line) {
122
278
  for (const part of this.parts) {
123
279
  const strokeWidth = part.style.stroke?.width;
@@ -83,7 +83,9 @@ class TextComponent extends AbstractComponent {
83
83
  }
84
84
  static applyTextStyles(ctx, style) {
85
85
  // Quote the font family if necessary.
86
- const fontFamily = style.fontFamily.match(/\s/) ? style.fontFamily.replace(/["]/g, '\\"') : style.fontFamily;
86
+ const hasSpaces = style.fontFamily.match(/\s/);
87
+ const isQuoted = style.fontFamily.match(/^".*"$/);
88
+ const fontFamily = hasSpaces && !isQuoted ? `"${style.fontFamily.replace(/["]/g, '\\"')}"` : style.fontFamily;
87
89
  ctx.font = [
88
90
  style.fontStyle ?? '',
89
91
  style.fontWeight ?? '',
@@ -9,7 +9,6 @@ import RenderingStyle from '../../rendering/RenderingStyle';
9
9
  /**
10
10
  * Creates strokes from line segments rather than Bézier curves.
11
11
  *
12
- * @beta Output behavior may change significantly between versions. For now, intended for debugging.
13
12
  */
14
13
  export declare const makePolylineBuilder: ComponentBuilderFactory;
15
14
  export default class PolylineBuilder implements ComponentBuilder {
@@ -21,6 +20,7 @@ export default class PolylineBuilder implements ComponentBuilder {
21
20
  private widthAverageNumSamples;
22
21
  private lastPoint;
23
22
  private startPoint;
23
+ private lastLineSegment;
24
24
  constructor(startPoint: StrokeDataPoint, minFitAllowed: number, viewport: Viewport);
25
25
  getBBox(): Rect2;
26
26
  protected getRenderingStyle(): RenderingStyle;
@@ -1,11 +1,10 @@
1
- import { Rect2, Color4, PathCommandType } from '@js-draw/math';
1
+ import { Rect2, Color4, PathCommandType, Vec2, LineSegment2 } from '@js-draw/math';
2
2
  import Stroke from '../Stroke.mjs';
3
3
  import Viewport from '../../Viewport.mjs';
4
4
  import makeShapeFitAutocorrect from './autocorrect/makeShapeFitAutocorrect.mjs';
5
5
  /**
6
6
  * Creates strokes from line segments rather than Bézier curves.
7
7
  *
8
- * @beta Output behavior may change significantly between versions. For now, intended for debugging.
9
8
  */
10
9
  export const makePolylineBuilder = makeShapeFitAutocorrect((initialPoint, viewport) => {
11
10
  const minFit = viewport.getSizeOfPixelOnCanvas();
@@ -17,6 +16,7 @@ export default class PolylineBuilder {
17
16
  this.viewport = viewport;
18
17
  this.parts = [];
19
18
  this.widthAverageNumSamples = 1;
19
+ this.lastLineSegment = null;
20
20
  this.averageWidth = startPoint.width;
21
21
  this.startPoint = {
22
22
  ...startPoint,
@@ -50,7 +50,7 @@ export default class PolylineBuilder {
50
50
  if (commands.length <= 1) {
51
51
  commands.push({
52
52
  kind: PathCommandType.LineTo,
53
- point: startPoint,
53
+ point: startPoint.plus(Vec2.of(this.averageWidth / 4, 0)),
54
54
  });
55
55
  }
56
56
  return {
@@ -98,11 +98,18 @@ export default class PolylineBuilder {
98
98
  + newPoint.width / this.widthAverageNumSamples;
99
99
  const roundedPoint = this.roundPoint(newPoint.pos);
100
100
  if (!roundedPoint.eq(this.lastPoint)) {
101
+ // If almost exactly in the same line as the previous
102
+ if (this.lastLineSegment && this.lastLineSegment.direction.dot(roundedPoint.minus(this.lastPoint).normalized()) > 0.997) {
103
+ this.parts.pop();
104
+ this.lastPoint = this.lastLineSegment.p1;
105
+ }
101
106
  this.parts.push({
102
107
  kind: PathCommandType.LineTo,
103
108
  point: this.roundPoint(newPoint.pos),
104
109
  });
105
110
  this.bbox = this.bbox.grownToPoint(roundedPoint);
111
+ this.lastLineSegment = new LineSegment2(this.lastPoint, roundedPoint);
112
+ this.lastPoint = roundedPoint;
106
113
  }
107
114
  }
108
115
  }
@@ -12,6 +12,7 @@ export default class PressureSensitiveFreehandLineBuilder implements ComponentBu
12
12
  private isFirstSegment;
13
13
  private pathStartConnector;
14
14
  private mostRecentConnector;
15
+ private nextCurveStartConnector;
15
16
  private upperSegments;
16
17
  private lowerSegments;
17
18
  private lastUpperBezier;
@@ -25,7 +26,6 @@ export default class PressureSensitiveFreehandLineBuilder implements ComponentBu
25
26
  private getRenderingStyle;
26
27
  private previewCurrentPath;
27
28
  private previewFullPath;
28
- private previewStroke;
29
29
  preview(renderer: AbstractRenderer): void;
30
30
  build(): Stroke;
31
31
  private roundPoint;
@@ -1,5 +1,4 @@
1
- import { Bezier } from 'bezier-js';
2
- import { Vec2, Rect2, PathCommandType } from '@js-draw/math';
1
+ import { Vec2, Rect2, PathCommandType, QuadraticBezier } from '@js-draw/math';
3
2
  import Stroke from '../Stroke.mjs';
4
3
  import Viewport from '../../Viewport.mjs';
5
4
  import { StrokeSmoother } from '../util/StrokeSmoother.mjs';
@@ -25,6 +24,7 @@ export default class PressureSensitiveFreehandLineBuilder {
25
24
  this.isFirstSegment = true;
26
25
  this.pathStartConnector = null;
27
26
  this.mostRecentConnector = null;
27
+ this.nextCurveStartConnector = null;
28
28
  this.lastUpperBezier = null;
29
29
  this.lastLowerBezier = null;
30
30
  this.parts = [];
@@ -42,18 +42,18 @@ export default class PressureSensitiveFreehandLineBuilder {
42
42
  fill: this.startPoint.color ?? null,
43
43
  };
44
44
  }
45
- previewCurrentPath() {
45
+ previewCurrentPath(extendWithLatest = true) {
46
46
  const upperPath = this.upperSegments.slice();
47
47
  const lowerPath = this.lowerSegments.slice();
48
48
  let lowerToUpperCap;
49
49
  let pathStartConnector;
50
50
  const currentCurve = this.curveFitter.preview();
51
- if (currentCurve) {
51
+ if (currentCurve && extendWithLatest) {
52
52
  const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand } = this.segmentToPath(currentCurve);
53
53
  upperPath.push(upperCurveCommand);
54
54
  lowerPath.push(lowerCurveCommand);
55
55
  lowerToUpperCap = lowerToUpperConnector;
56
- pathStartConnector = this.pathStartConnector ?? upperToLowerConnector;
56
+ pathStartConnector = this.pathStartConnector ?? [upperToLowerConnector];
57
57
  }
58
58
  else {
59
59
  if (this.mostRecentConnector === null || this.pathStartConnector === null) {
@@ -94,7 +94,7 @@ export default class PressureSensitiveFreehandLineBuilder {
94
94
  // __/ __/
95
95
  // /___ /
96
96
  // •
97
- pathStartConnector,
97
+ ...pathStartConnector,
98
98
  // Move back to the start point:
99
99
  // •
100
100
  // __/ __/
@@ -111,13 +111,6 @@ export default class PressureSensitiveFreehandLineBuilder {
111
111
  }
112
112
  return null;
113
113
  }
114
- previewStroke() {
115
- const pathPreview = this.previewFullPath();
116
- if (pathPreview) {
117
- return new Stroke(pathPreview);
118
- }
119
- return null;
120
- }
121
114
  preview(renderer) {
122
115
  const paths = this.previewFullPath();
123
116
  if (paths) {
@@ -135,7 +128,7 @@ export default class PressureSensitiveFreehandLineBuilder {
135
128
  // Ensure we have something.
136
129
  this.addCurve(null);
137
130
  }
138
- return this.previewStroke();
131
+ return new Stroke(this.previewFullPath());
139
132
  }
140
133
  roundPoint(point) {
141
134
  let minFit = Math.min(this.minFitAllowed, this.curveStartWidth / 3);
@@ -150,25 +143,16 @@ export default class PressureSensitiveFreehandLineBuilder {
150
143
  return false;
151
144
  }
152
145
  const getIntersection = (curve1, curve2) => {
153
- const intersection = curve1.intersects(curve2);
154
- if (!intersection || intersection.length === 0) {
146
+ const intersections = curve1.intersectsBezier(curve2);
147
+ if (!intersections.length)
155
148
  return null;
156
- }
157
- // From http://pomax.github.io/bezierjs/#intersect-curve,
158
- // .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
159
- const firstTPair = intersection[0];
160
- const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(firstTPair);
161
- if (!match) {
162
- throw new Error(`Incorrect format returned by .intersects: ${intersection} should be array of "number/number"!`);
163
- }
164
- const t = parseFloat(match[1]);
165
- return Vec2.ofXY(curve1.get(t));
149
+ return intersections[0].point;
166
150
  };
167
151
  const getExitDirection = (curve) => {
168
- return Vec2.ofXY(curve.points[2]).minus(Vec2.ofXY(curve.points[1])).normalized();
152
+ return curve.p2.minus(curve.p1).normalized();
169
153
  };
170
154
  const getEnterDirection = (curve) => {
171
- return Vec2.ofXY(curve.points[1]).minus(Vec2.ofXY(curve.points[0])).normalized();
155
+ return curve.p1.minus(curve.p0).normalized();
172
156
  };
173
157
  // Prevent
174
158
  // /
@@ -179,8 +163,8 @@ export default class PressureSensitiveFreehandLineBuilder {
179
163
  // where the next stroke and the previous stroke are in different directions.
180
164
  //
181
165
  // Are the exit/enter directions of the previous and current curves in different enough directions?
182
- if (getEnterDirection(upperCurve).dot(getExitDirection(this.lastUpperBezier)) < 0.3
183
- || getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.3
166
+ if (getEnterDirection(upperCurve).dot(getExitDirection(this.lastUpperBezier)) < 0.35
167
+ || getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.35
184
168
  // Also handle if the curves exit/enter directions differ
185
169
  || getEnterDirection(upperCurve).dot(getExitDirection(upperCurve)) < 0
186
170
  || getEnterDirection(lowerCurve).dot(getExitDirection(lowerCurve)) < 0) {
@@ -236,32 +220,37 @@ export default class PressureSensitiveFreehandLineBuilder {
236
220
  controlPoint: center.plus(Vec2.of(width, -width)),
237
221
  endPoint: center.plus(Vec2.of(width, 0)),
238
222
  });
239
- this.pathStartConnector = {
223
+ const connector = {
240
224
  kind: PathCommandType.LineTo,
241
225
  point: startPoint,
242
226
  };
243
- this.mostRecentConnector = this.pathStartConnector;
227
+ this.pathStartConnector = [connector];
228
+ this.mostRecentConnector = connector;
244
229
  return;
245
230
  }
246
- const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand, lowerCurve, upperCurve, } = this.segmentToPath(curve);
247
- const shouldStartNew = this.shouldStartNewSegment(lowerCurve, upperCurve);
231
+ const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand, lowerCurve, upperCurve, nextCurveStartConnector, } = this.segmentToPath(curve);
232
+ let shouldStartNew = this.shouldStartNewSegment(lowerCurve, upperCurve);
248
233
  if (shouldStartNew) {
249
- const part = this.previewCurrentPath();
234
+ const part = this.previewCurrentPath(false);
250
235
  if (part) {
251
236
  this.parts.push(part);
252
237
  this.upperSegments = [];
253
238
  this.lowerSegments = [];
254
239
  }
240
+ else {
241
+ shouldStartNew = false;
242
+ }
255
243
  }
256
244
  if (this.isFirstSegment || shouldStartNew) {
257
245
  // We draw the upper path (reversed), then the lower path, so we need the
258
246
  // upperToLowerConnector to join the two paths.
259
- this.pathStartConnector = upperToLowerConnector;
247
+ this.pathStartConnector = this.nextCurveStartConnector ?? [upperToLowerConnector];
260
248
  this.isFirstSegment = false;
261
249
  }
262
250
  // With the most recent connector, we're joining the end of the lowerPath to the most recent
263
251
  // upperPath:
264
252
  this.mostRecentConnector = lowerToUpperConnector;
253
+ this.nextCurveStartConnector = nextCurveStartConnector;
265
254
  this.lowerSegments.push(lowerCurveCommand);
266
255
  this.upperSegments.push(upperCurveCommand);
267
256
  this.lastLowerBezier = lowerCurve;
@@ -270,9 +259,9 @@ export default class PressureSensitiveFreehandLineBuilder {
270
259
  }
271
260
  // Returns [upper curve, connector, lower curve]
272
261
  segmentToPath(curve) {
273
- const bezier = new Bezier(curve.startPoint.xy, curve.controlPoint.xy, curve.endPoint.xy);
274
- let startVec = Vec2.ofXY(bezier.normal(0)).normalized();
275
- let endVec = Vec2.ofXY(bezier.normal(1)).normalized();
262
+ const bezier = new QuadraticBezier(curve.startPoint, curve.controlPoint, curve.endPoint);
263
+ let startVec = bezier.normal(0);
264
+ let endVec = bezier.normal(1);
276
265
  startVec = startVec.times(curve.startWidth / 2);
277
266
  endVec = endVec.times(curve.endWidth / 2);
278
267
  if (!isFinite(startVec.magnitude())) {
@@ -283,18 +272,9 @@ export default class PressureSensitiveFreehandLineBuilder {
283
272
  const endPt = curve.endPoint;
284
273
  const controlPoint = curve.controlPoint;
285
274
  // Approximate the normal at the location of the control point
286
- let projectionT = bezier.project(controlPoint.xy).t;
287
- if (!projectionT) {
288
- if (startPt.squareDistanceTo(controlPoint) < endPt.squareDistanceTo(controlPoint)) {
289
- projectionT = 0.1;
290
- }
291
- else {
292
- projectionT = 0.9;
293
- }
294
- }
275
+ const projectionT = bezier.nearestPointTo(controlPoint).parameterValue;
295
276
  const halfVecT = projectionT;
296
- const halfVec = Vec2.ofXY(bezier.normal(halfVecT))
297
- .normalized().times(curve.startWidth / 2 * halfVecT
277
+ const halfVec = bezier.normal(halfVecT).times(curve.startWidth / 2 * halfVecT
298
278
  + curve.endWidth / 2 * (1 - halfVecT));
299
279
  // Each starts at startPt ± startVec
300
280
  const lowerCurveStartPoint = this.roundPoint(startPt.plus(startVec));
@@ -318,16 +298,29 @@ export default class PressureSensitiveFreehandLineBuilder {
318
298
  kind: PathCommandType.LineTo,
319
299
  point: upperCurveStartPoint,
320
300
  };
301
+ // The segment to be used to start the next path (to insert to connect the start of its
302
+ // lower and the end of its upper).
303
+ const nextCurveStartConnector = [
304
+ {
305
+ kind: PathCommandType.LineTo,
306
+ point: upperCurveStartPoint,
307
+ },
308
+ {
309
+ kind: PathCommandType.LineTo,
310
+ point: lowerCurveEndPoint,
311
+ },
312
+ ];
321
313
  const upperCurveCommand = {
322
314
  kind: PathCommandType.QuadraticBezierTo,
323
315
  controlPoint: upperCurveControlPoint,
324
316
  endPoint: upperCurveEndPoint,
325
317
  };
326
- const upperCurve = new Bezier(upperCurveStartPoint, upperCurveControlPoint, upperCurveEndPoint);
327
- const lowerCurve = new Bezier(lowerCurveStartPoint, lowerCurveControlPoint, lowerCurveEndPoint);
318
+ const upperCurve = new QuadraticBezier(upperCurveStartPoint, upperCurveControlPoint, upperCurveEndPoint);
319
+ const lowerCurve = new QuadraticBezier(lowerCurveStartPoint, lowerCurveControlPoint, lowerCurveEndPoint);
328
320
  return {
329
321
  upperCurveCommand, upperToLowerConnector, lowerToUpperConnector, lowerCurveCommand,
330
322
  upperCurve, lowerCurve,
323
+ nextCurveStartConnector,
331
324
  };
332
325
  }
333
326
  addPoint(newPoint) {
@@ -629,7 +629,7 @@ export class ImageNode {
629
629
  this.bbox = Rect2.union(...this.children.map(child => child.getBBox()));
630
630
  }
631
631
  if (bubbleUp && !oldBBox.eq(this.bbox)) {
632
- if (!this.bbox.containsRect(oldBBox)) {
632
+ if (this.bbox.containsRect(oldBBox)) {
633
633
  this.parent?.unionBBoxWith(this.bbox);
634
634
  }
635
635
  else {
@@ -27,7 +27,7 @@ const localization = {
27
27
  selectionToolKeyboardShortcuts: 'Auswahl-Werkzeug: Verwende die Pfeiltasten, um ausgewählte Elemente zu verschieben und ‚i‘ und ‚o‘, um ihre Größe zu ändern.',
28
28
  touchPanning: 'Ansicht mit Touchscreen verschieben',
29
29
  anyDevicePanning: 'Ansicht mit jedem Eingabegerät verschieben',
30
- selectPenTip: 'Objekt-Typ: ',
30
+ selectPenType: 'Objekt-Typ: ',
31
31
  roundedTipPen: 'Freihand',
32
32
  flatTipPen: 'Stift (druckempfindlich)',
33
33
  arrowPen: 'Pfeil',
@@ -22,7 +22,7 @@ const localization = {
22
22
  save: 'Guardar',
23
23
  undo: 'Deshace',
24
24
  redo: 'Rehace',
25
- selectPenTip: 'Punta',
25
+ selectPenType: 'Punta',
26
26
  selectShape: 'Forma',
27
27
  pickColorFromScreen: 'Selecciona un color de la pantalla',
28
28
  clickToPickColorAnnouncement: 'Haga un clic en la pantalla para seleccionar un color',
@@ -139,24 +139,29 @@ export default class RenderingCacheNode {
139
139
  || items.length === 0) {
140
140
  return;
141
141
  }
142
- const newItems = [];
143
- // Divide [items] until nodes are leaves or smaller than this
144
- for (const item of items) {
145
- const bbox = item.getBBox();
146
- if (!bbox.intersects(this.region)) {
147
- continue;
148
- }
149
- if (bbox.maxDimension >= this.region.maxDimension) {
150
- newItems.push(...item.getChildrenOrSelfIntersectingRegion(this.region));
151
- }
152
- else {
153
- newItems.push(item);
142
+ // Divide [items] until nodes are smaller than this, or are leaves.
143
+ const divideUntilSmallerThanThis = (itemsToDivide) => {
144
+ const newItems = [];
145
+ for (const item of itemsToDivide) {
146
+ const bbox = item.getBBox();
147
+ if (!bbox.intersects(this.region)) {
148
+ continue;
149
+ }
150
+ if (bbox.maxDimension >= this.region.maxDimension) {
151
+ newItems.push(...item.getChildrenOrSelfIntersectingRegion(this.region));
152
+ }
153
+ else {
154
+ newItems.push(item);
155
+ }
154
156
  }
155
- }
156
- items = newItems;
157
+ return newItems;
158
+ };
159
+ items = divideUntilSmallerThanThis(items);
157
160
  // Can we cache at all?
158
161
  if (!this.cacheState.props.isOfCorrectType(screenRenderer)) {
159
- items.forEach(item => item.render(screenRenderer, viewport.visibleRect));
162
+ for (const item of items) {
163
+ item.render(screenRenderer, viewport.visibleRect);
164
+ }
160
165
  return;
161
166
  }
162
167
  if (this.cacheState.debugMode) {
@@ -1,4 +1,4 @@
1
- import Editor from '../Editor';
1
+ import Editor, { EditorSettings } from '../Editor';
2
2
  /** Creates an editor. Should only be used in test files. */
3
- declare const _default: () => Editor;
3
+ declare const _default: (settings?: Partial<EditorSettings>) => Editor;
4
4
  export default _default;
@@ -1,9 +1,9 @@
1
1
  import { RenderingMode } from '../rendering/Display.mjs';
2
2
  import Editor from '../Editor.mjs';
3
3
  /** Creates an editor. Should only be used in test files. */
4
- export default () => {
4
+ export default (settings) => {
5
5
  if (jest === undefined) {
6
6
  throw new Error('Files in the testing/ folder should only be used in tests!');
7
7
  }
8
- return new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });
8
+ return new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer, ...settings });
9
9
  };
@@ -0,0 +1,3 @@
1
+ /** Returns the first node or element with `textContent` matching `expectedText`. */
2
+ declare const findNodeWithText: (expectedText: string, parent: Node) => Node | null;
3
+ export default findNodeWithText;
@@ -0,0 +1,14 @@
1
+ /** Returns the first node or element with `textContent` matching `expectedText`. */
2
+ const findNodeWithText = (expectedText, parent) => {
3
+ if (parent.textContent === expectedText) {
4
+ return parent;
5
+ }
6
+ for (const child of parent.childNodes) {
7
+ const results = findNodeWithText(expectedText, child);
8
+ if (results) {
9
+ return results;
10
+ }
11
+ }
12
+ return null;
13
+ };
14
+ export default findNodeWithText;
@@ -0,0 +1,3 @@
1
+ /** Returns the first ancestor of the given node that is an HTMLElement */
2
+ declare const firstElementAncestorOfNode: (node: Node | null) => HTMLElement | null;
3
+ export default firstElementAncestorOfNode;
@@ -0,0 +1,11 @@
1
+ /** Returns the first ancestor of the given node that is an HTMLElement */
2
+ const firstElementAncestorOfNode = (node) => {
3
+ if (node instanceof HTMLElement) {
4
+ return node;
5
+ }
6
+ else if (node?.parentNode) {
7
+ return firstElementAncestorOfNode(node.parentNode);
8
+ }
9
+ return null;
10
+ };
11
+ export default firstElementAncestorOfNode;
@@ -0,0 +1,3 @@
1
+ import type Editor from '../Editor';
2
+ declare const sendKeyPressRelease: (editor: Editor, key: string) => void;
3
+ export default sendKeyPressRelease;