js-draw 0.1.11 → 0.1.12

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 (167) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Editor.d.ts +4 -2
  4. package/dist/src/Editor.js +30 -10
  5. package/dist/src/EditorImage.d.ts +1 -1
  6. package/dist/src/EditorImage.js +2 -2
  7. package/dist/src/Pointer.d.ts +1 -1
  8. package/dist/src/Pointer.js +1 -1
  9. package/dist/src/SVGLoader.d.ts +1 -1
  10. package/dist/src/SVGLoader.js +14 -6
  11. package/dist/src/Viewport.d.ts +8 -25
  12. package/dist/src/Viewport.js +15 -10
  13. package/dist/src/commands/Command.d.ts +2 -2
  14. package/dist/src/commands/Command.js +4 -4
  15. package/dist/src/commands/Duplicate.d.ts +1 -1
  16. package/dist/src/commands/Duplicate.js +1 -1
  17. package/dist/src/commands/Erase.d.ts +1 -1
  18. package/dist/src/commands/Erase.js +1 -1
  19. package/dist/src/commands/localization.d.ts +1 -1
  20. package/dist/src/components/AbstractComponent.d.ts +3 -3
  21. package/dist/src/components/AbstractComponent.js +2 -2
  22. package/dist/src/components/SVGGlobalAttributesObject.d.ts +3 -3
  23. package/dist/src/components/SVGGlobalAttributesObject.js +1 -1
  24. package/dist/src/components/Stroke.d.ts +4 -4
  25. package/dist/src/components/Stroke.js +2 -2
  26. package/dist/src/components/Text.d.ts +3 -3
  27. package/dist/src/components/Text.js +3 -3
  28. package/dist/src/components/UnknownSVGObject.d.ts +3 -3
  29. package/dist/src/components/UnknownSVGObject.js +1 -1
  30. package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
  31. package/dist/src/components/builders/ArrowBuilder.js +1 -1
  32. package/dist/src/components/builders/FreehandLineBuilder.d.ts +8 -3
  33. package/dist/src/components/builders/FreehandLineBuilder.js +142 -71
  34. package/dist/src/components/builders/LineBuilder.d.ts +1 -1
  35. package/dist/src/components/builders/LineBuilder.js +1 -1
  36. package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
  37. package/dist/src/components/builders/RectangleBuilder.js +3 -3
  38. package/dist/src/components/builders/types.d.ts +1 -1
  39. package/dist/src/localization.d.ts +1 -0
  40. package/dist/src/localization.js +5 -1
  41. package/dist/src/localizations/es.js +1 -1
  42. package/dist/src/{geometry → math}/LineSegment2.d.ts +0 -0
  43. package/dist/src/{geometry → math}/LineSegment2.js +0 -0
  44. package/dist/src/{geometry → math}/Mat33.d.ts +0 -0
  45. package/dist/src/{geometry → math}/Mat33.js +0 -0
  46. package/dist/src/{geometry → math}/Path.d.ts +2 -1
  47. package/dist/src/{geometry → math}/Path.js +58 -51
  48. package/dist/src/{geometry → math}/Rect2.d.ts +0 -0
  49. package/dist/src/{geometry → math}/Rect2.js +0 -0
  50. package/dist/src/{geometry → math}/Vec2.d.ts +0 -0
  51. package/dist/src/{geometry → math}/Vec2.js +0 -0
  52. package/dist/src/{geometry → math}/Vec3.d.ts +1 -1
  53. package/dist/src/{geometry → math}/Vec3.js +1 -1
  54. package/dist/src/math/rounding.d.ts +3 -0
  55. package/dist/src/math/rounding.js +120 -0
  56. package/dist/src/rendering/Display.d.ts +3 -1
  57. package/dist/src/rendering/Display.js +16 -10
  58. package/dist/src/rendering/caching/CacheRecord.d.ts +2 -2
  59. package/dist/src/rendering/caching/CacheRecord.js +1 -1
  60. package/dist/src/rendering/caching/CacheRecordManager.d.ts +1 -1
  61. package/dist/src/rendering/caching/RenderingCache.js +1 -1
  62. package/dist/src/rendering/caching/RenderingCacheNode.d.ts +2 -1
  63. package/dist/src/rendering/caching/RenderingCacheNode.js +18 -7
  64. package/dist/src/rendering/caching/testUtils.js +1 -1
  65. package/dist/src/rendering/caching/types.d.ts +1 -1
  66. package/dist/src/rendering/localization.d.ts +2 -0
  67. package/dist/src/rendering/localization.js +2 -0
  68. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +4 -4
  69. package/dist/src/rendering/renderers/AbstractRenderer.js +2 -2
  70. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +4 -4
  71. package/dist/src/rendering/renderers/CanvasRenderer.js +1 -1
  72. package/dist/src/rendering/renderers/DummyRenderer.d.ts +4 -4
  73. package/dist/src/rendering/renderers/DummyRenderer.js +1 -1
  74. package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -3
  75. package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
  76. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +5 -3
  77. package/dist/src/rendering/renderers/TextOnlyRenderer.js +13 -3
  78. package/dist/src/toolbar/icons.d.ts +3 -0
  79. package/dist/src/toolbar/icons.js +142 -132
  80. package/dist/src/toolbar/localization.d.ts +2 -1
  81. package/dist/src/toolbar/localization.js +2 -1
  82. package/dist/src/toolbar/makeColorInput.js +2 -1
  83. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +13 -0
  84. package/dist/src/toolbar/widgets/ActionButtonWidget.js +21 -0
  85. package/dist/src/toolbar/widgets/BaseWidget.js +2 -0
  86. package/dist/src/toolbar/widgets/HandToolWidget.js +3 -3
  87. package/dist/src/toolbar/widgets/SelectionWidget.d.ts +0 -1
  88. package/dist/src/toolbar/widgets/SelectionWidget.js +23 -30
  89. package/dist/src/tools/Eraser.js +1 -1
  90. package/dist/src/tools/PanZoom.d.ts +1 -1
  91. package/dist/src/tools/PanZoom.js +24 -14
  92. package/dist/src/tools/SelectionTool.d.ts +3 -3
  93. package/dist/src/tools/SelectionTool.js +6 -6
  94. package/dist/src/tools/TextTool.js +1 -1
  95. package/dist/src/types.d.ts +4 -4
  96. package/package.json +1 -1
  97. package/src/Editor.ts +34 -12
  98. package/src/EditorImage.test.ts +2 -4
  99. package/src/EditorImage.ts +2 -2
  100. package/src/Pointer.ts +1 -1
  101. package/src/SVGLoader.ts +14 -6
  102. package/src/Viewport.ts +19 -17
  103. package/src/commands/Command.ts +5 -5
  104. package/src/commands/Duplicate.ts +1 -1
  105. package/src/commands/Erase.ts +1 -1
  106. package/src/commands/localization.ts +1 -1
  107. package/src/components/AbstractComponent.ts +4 -4
  108. package/src/components/SVGGlobalAttributesObject.ts +3 -3
  109. package/src/components/Stroke.test.ts +3 -5
  110. package/src/components/Stroke.ts +4 -4
  111. package/src/components/Text.test.ts +2 -2
  112. package/src/components/Text.ts +3 -3
  113. package/src/components/UnknownSVGObject.ts +3 -3
  114. package/src/components/builders/ArrowBuilder.ts +2 -2
  115. package/src/components/builders/FreehandLineBuilder.ts +190 -80
  116. package/src/components/builders/LineBuilder.ts +2 -2
  117. package/src/components/builders/RectangleBuilder.ts +3 -3
  118. package/src/components/builders/types.ts +1 -1
  119. package/src/localization.ts +6 -0
  120. package/src/localizations/es.ts +2 -1
  121. package/src/{geometry → math}/LineSegment2.test.ts +0 -0
  122. package/src/{geometry → math}/LineSegment2.ts +0 -0
  123. package/src/{geometry → math}/Mat33.test.ts +0 -0
  124. package/src/{geometry → math}/Mat33.ts +0 -0
  125. package/src/{geometry → math}/Path.fromString.test.ts +0 -0
  126. package/src/{geometry → math}/Path.test.ts +0 -0
  127. package/src/{geometry → math}/Path.toString.test.ts +11 -2
  128. package/src/{geometry → math}/Path.ts +60 -57
  129. package/src/{geometry → math}/Rect2.test.ts +0 -0
  130. package/src/{geometry → math}/Rect2.ts +0 -0
  131. package/src/{geometry → math}/Vec2.test.ts +0 -0
  132. package/src/{geometry → math}/Vec2.ts +0 -0
  133. package/src/{geometry → math}/Vec3.test.ts +0 -0
  134. package/src/{geometry → math}/Vec3.ts +2 -2
  135. package/src/math/rounding.test.ts +40 -0
  136. package/src/math/rounding.ts +145 -0
  137. package/src/rendering/Display.ts +18 -10
  138. package/src/rendering/caching/CacheRecord.test.ts +2 -2
  139. package/src/rendering/caching/CacheRecord.ts +2 -2
  140. package/src/rendering/caching/CacheRecordManager.ts +1 -1
  141. package/src/rendering/caching/RenderingCache.test.ts +3 -3
  142. package/src/rendering/caching/RenderingCache.ts +1 -1
  143. package/src/rendering/caching/RenderingCacheNode.ts +23 -7
  144. package/src/rendering/caching/testUtils.ts +1 -1
  145. package/src/rendering/caching/types.ts +1 -1
  146. package/src/rendering/localization.ts +4 -0
  147. package/src/rendering/renderers/AbstractRenderer.ts +4 -4
  148. package/src/rendering/renderers/CanvasRenderer.ts +4 -4
  149. package/src/rendering/renderers/DummyRenderer.test.ts +2 -2
  150. package/src/rendering/renderers/DummyRenderer.ts +4 -4
  151. package/src/rendering/renderers/SVGRenderer.ts +10 -4
  152. package/src/rendering/renderers/TextOnlyRenderer.ts +17 -6
  153. package/src/toolbar/icons.ts +157 -137
  154. package/src/toolbar/localization.ts +4 -2
  155. package/src/toolbar/makeColorInput.ts +2 -1
  156. package/src/toolbar/toolbar.css +1 -1
  157. package/src/toolbar/widgets/ActionButtonWidget.ts +31 -0
  158. package/src/toolbar/widgets/BaseWidget.ts +2 -0
  159. package/src/toolbar/widgets/HandToolWidget.ts +3 -3
  160. package/src/toolbar/widgets/SelectionWidget.ts +46 -41
  161. package/src/tools/Eraser.ts +2 -2
  162. package/src/tools/PanZoom.ts +28 -16
  163. package/src/tools/SelectionTool.test.ts +2 -4
  164. package/src/tools/SelectionTool.ts +6 -6
  165. package/src/tools/TextTool.ts +2 -2
  166. package/src/tools/UndoRedoShortcut.test.ts +1 -1
  167. package/src/types.ts +4 -4
@@ -1,9 +1,9 @@
1
1
  import { Bezier } from 'bezier-js';
2
2
  import AbstractRenderer, { RenderablePathSpec } from '../../rendering/renderers/AbstractRenderer';
3
- import { Point2, Vec2 } from '../../geometry/Vec2';
4
- import Rect2 from '../../geometry/Rect2';
5
- import { PathCommand, PathCommandType } from '../../geometry/Path';
6
- import LineSegment2 from '../../geometry/LineSegment2';
3
+ import { Point2, Vec2 } from '../../math/Vec2';
4
+ import Rect2 from '../../math/Rect2';
5
+ import { LinePathCommand, PathCommandType, QuadraticBezierPathCommand } from '../../math/Path';
6
+ import LineSegment2 from '../../math/LineSegment2';
7
7
  import Stroke from '../Stroke';
8
8
  import Viewport from '../../Viewport';
9
9
  import { StrokeDataPoint } from '../../types';
@@ -22,9 +22,35 @@ export const makeFreehandLineBuilder: ComponentBuilderFactory = (initialPoint: S
22
22
  );
23
23
  };
24
24
 
25
+ type CurrentSegmentToPathResult = {
26
+ upperCurve: QuadraticBezierPathCommand,
27
+ lowerToUpperConnector: LinePathCommand,
28
+ upperToLowerConnector: LinePathCommand,
29
+ lowerCurve: QuadraticBezierPathCommand,
30
+ };
31
+
25
32
  // Handles stroke smoothing and creates Strokes from user/stylus input.
26
33
  export default class FreehandLineBuilder implements ComponentBuilder {
27
- private segments: RenderablePathSpec[];
34
+ private isFirstSegment: boolean = true;
35
+ private pathStartConnector: LinePathCommand|null = null;
36
+ private mostRecentConnector: LinePathCommand|null = null;
37
+
38
+ // Beginning of the list of lower parts
39
+ // ↓
40
+ // /---pathStartConnector---/ ← Beginning of the list of upper parts
41
+ // ___/ __/
42
+ // / /
43
+ // /--Most recent connector--/ ← most recent upper part goes here
44
+ // ↑
45
+ // most recent lower part goes here
46
+ //
47
+ // The upperSegments form a path that goes in reverse from the most recent edge to the
48
+ // least recent edge.
49
+ // The lowerSegments form a path that goes from the least recent edge to the most
50
+ // recent edge.
51
+ private upperSegments: QuadraticBezierPathCommand[];
52
+ private lowerSegments: QuadraticBezierPathCommand[];
53
+
28
54
  private buffer: Point2[];
29
55
  private lastPoint: StrokeDataPoint;
30
56
  private lastExitingVec: Vec2;
@@ -47,7 +73,9 @@ export default class FreehandLineBuilder implements ComponentBuilder {
47
73
  private maxFitAllowed: number
48
74
  ) {
49
75
  this.lastPoint = this.startPoint;
50
- this.segments = [];
76
+ this.upperSegments = [];
77
+ this.lowerSegments = [];
78
+
51
79
  this.buffer = [this.startPoint.pos];
52
80
  this.momentum = Vec2.zero;
53
81
  this.currentCurve = null;
@@ -65,19 +93,82 @@ export default class FreehandLineBuilder implements ComponentBuilder {
65
93
  };
66
94
  }
67
95
 
68
- // Get the segments that make up this' path. Can be called after calling build()
69
- private getPreview(): RenderablePathSpec[] {
70
- if (this.currentCurve && this.lastPoint) {
71
- const currentPath = this.currentSegmentToPath();
72
- return this.segments.concat(currentPath);
96
+ private previewPath(): RenderablePathSpec|null {
97
+ let upperPath: QuadraticBezierPathCommand[];
98
+ let lowerPath: QuadraticBezierPathCommand[];
99
+ let lowerToUpperCap: LinePathCommand;
100
+ let pathStartConnector: LinePathCommand;
101
+ if (this.currentCurve) {
102
+ const { upperCurve, lowerToUpperConnector, upperToLowerConnector, lowerCurve } = this.currentSegmentToPath();
103
+ upperPath = this.upperSegments.concat(upperCurve);
104
+ lowerPath = this.lowerSegments.concat(lowerCurve);
105
+ lowerToUpperCap = lowerToUpperConnector;
106
+ pathStartConnector = this.pathStartConnector ?? upperToLowerConnector;
107
+ } else {
108
+ if (this.mostRecentConnector === null || this.pathStartConnector === null) {
109
+ return null;
110
+ }
111
+
112
+ upperPath = this.upperSegments.slice();
113
+ lowerPath = this.lowerSegments.slice();
114
+ lowerToUpperCap = this.mostRecentConnector;
115
+ pathStartConnector = this.pathStartConnector;
73
116
  }
117
+ const startPoint = lowerPath[lowerPath.length - 1].endPoint;
74
118
 
75
- return this.segments;
119
+
120
+ return {
121
+ // Start at the end of the lower curve:
122
+ // Start point
123
+ // ↓
124
+ // __/ __/ ← Most recent points on this end
125
+ // /___ /
126
+ // ↑
127
+ // Oldest points
128
+ startPoint,
129
+
130
+ commands: [
131
+ // Move to the most recent point on the upperPath:
132
+ // ----→•
133
+ // __/ __/
134
+ // /___ /
135
+ lowerToUpperCap,
136
+
137
+ // Move to the beginning of the upperPath:
138
+ // __/ __/
139
+ // /___ /
140
+ // • ←-
141
+ ...upperPath.reverse(),
142
+
143
+ // Move to the beginning of the lowerPath:
144
+ // __/ __/
145
+ // /___ /
146
+ // •
147
+ pathStartConnector,
148
+
149
+ // Move back to the start point:
150
+ // •
151
+ // __/ __/
152
+ // /___ /
153
+ ...lowerPath,
154
+ ],
155
+ style: this.getRenderingStyle(),
156
+ };
157
+ }
158
+
159
+ private previewStroke(): Stroke|null {
160
+ const pathPreview = this.previewPath();
161
+
162
+ if (pathPreview) {
163
+ return new Stroke([ pathPreview ]);
164
+ }
165
+ return null;
76
166
  }
77
167
 
78
168
  public preview(renderer: AbstractRenderer) {
79
- for (const part of this.getPreview()) {
80
- renderer.drawPath(part);
169
+ const path = this.previewPath();
170
+ if (path) {
171
+ renderer.drawPath(path);
81
172
  }
82
173
  }
83
174
 
@@ -85,9 +176,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
85
176
  if (this.lastPoint) {
86
177
  this.finalizeCurrentCurve();
87
178
  }
88
- return new Stroke(
89
- this.segments,
90
- );
179
+ return this.previewStroke()!;
91
180
  }
92
181
 
93
182
  private roundPoint(point: Point2): Point2 {
@@ -98,59 +187,77 @@ export default class FreehandLineBuilder implements ComponentBuilder {
98
187
  // Case where no points have been added
99
188
  if (!this.currentCurve) {
100
189
  // Don't create a circle around the initial point if the stroke has more than one point.
101
- if (this.segments.length > 0) {
190
+ if (!this.isFirstSegment) {
102
191
  return;
103
192
  }
104
193
 
105
- const width = Viewport.roundPoint(this.startPoint.width / 3, this.minFitAllowed);
194
+ const width = Viewport.roundPoint(this.startPoint.width / 3.5, this.minFitAllowed);
106
195
  const center = this.roundPoint(this.startPoint.pos);
107
196
 
197
+ // Start on the right, cycle clockwise:
198
+ // |
199
+ // ----- ←
200
+ // |
201
+ const startPoint = this.startPoint.pos.plus(Vec2.of(width, 0));
202
+
108
203
  // Draw a circle-ish shape around the start point
109
- this.segments.push({
110
- // Start on the right, cycle clockwise:
111
- // |
112
- // -----
113
- // |
114
- startPoint: this.startPoint.pos.plus(Vec2.of(width, 0)),
115
- commands: [
116
- {
117
- kind: PathCommandType.QuadraticBezierTo,
118
- controlPoint: center.plus(Vec2.of(width, width)),
119
-
120
- // Bottom of the circle
121
- // |
122
- // -----
123
- // |
124
- // ↑
125
- endPoint: center.plus(Vec2.of(0, width)),
126
- },
127
- {
128
- kind: PathCommandType.QuadraticBezierTo,
129
- controlPoint: center.plus(Vec2.of(-width, width)),
130
- endPoint: center.plus(Vec2.of(-width, 0)),
131
- },
132
- {
133
- kind: PathCommandType.QuadraticBezierTo,
134
- controlPoint: center.plus(Vec2.of(-width, -width)),
135
- endPoint: center.plus(Vec2.of(0, -width)),
136
- },
137
- {
138
- kind: PathCommandType.QuadraticBezierTo,
139
- controlPoint: center.plus(Vec2.of(width, -width)),
140
- endPoint: center.plus(Vec2.of(width, 0)),
141
- },
142
- ],
143
- style: this.getRenderingStyle(),
144
- });
204
+ this.lowerSegments.push(
205
+ {
206
+ kind: PathCommandType.QuadraticBezierTo,
207
+ controlPoint: center.plus(Vec2.of(width, width)),
208
+
209
+ // Bottom of the circle
210
+ // |
211
+ // -----
212
+ // |
213
+ // ↑
214
+ endPoint: center.plus(Vec2.of(0, width)),
215
+ },
216
+ {
217
+ kind: PathCommandType.QuadraticBezierTo,
218
+ controlPoint: center.plus(Vec2.of(-width, width)),
219
+ endPoint: center.plus(Vec2.of(-width, 0)),
220
+ },
221
+ {
222
+ kind: PathCommandType.QuadraticBezierTo,
223
+ controlPoint: center.plus(Vec2.of(-width, -width)),
224
+ endPoint: center.plus(Vec2.of(0, -width)),
225
+ },
226
+ {
227
+ kind: PathCommandType.QuadraticBezierTo,
228
+ controlPoint: center.plus(Vec2.of(width, -width)),
229
+ endPoint: center.plus(Vec2.of(width, 0)),
230
+ }
231
+ );
232
+ this.pathStartConnector = {
233
+ kind: PathCommandType.LineTo,
234
+ point: startPoint,
235
+ };
236
+ this.mostRecentConnector = this.pathStartConnector;
237
+
145
238
  return;
146
239
  }
147
240
 
148
- this.segments.push(this.currentSegmentToPath());
241
+ const { upperCurve, lowerToUpperConnector, upperToLowerConnector, lowerCurve } = this.currentSegmentToPath();
242
+
243
+ if (this.isFirstSegment) {
244
+ // We draw the upper path (reversed), then the lower path, so we need the
245
+ // upperToLowerConnector to join the two paths.
246
+ this.pathStartConnector = upperToLowerConnector;
247
+ this.isFirstSegment = false;
248
+ }
249
+ // With the most recent connector, we're joining the end of the lowerPath to the most recent
250
+ // upperPath:
251
+ this.mostRecentConnector = lowerToUpperConnector;
252
+
253
+ this.upperSegments.push(upperCurve);
254
+ this.lowerSegments.push(lowerCurve);
255
+
149
256
  const lastPoint = this.buffer[this.buffer.length - 1];
150
257
  this.lastExitingVec = Vec2.ofXY(
151
258
  this.currentCurve.points[2]
152
259
  ).minus(Vec2.ofXY(this.currentCurve.points[1]));
153
- console.assert(this.lastExitingVec.magnitude() !== 0);
260
+ console.assert(this.lastExitingVec.magnitude() !== 0, 'lastExitingVec has zero length!');
154
261
 
155
262
  // Use the last two points to start a new curve (the last point isn't used
156
263
  // in the current curve and we want connected curves to share end points)
@@ -160,7 +267,8 @@ export default class FreehandLineBuilder implements ComponentBuilder {
160
267
  this.currentCurve = null;
161
268
  }
162
269
 
163
- private currentSegmentToPath(): RenderablePathSpec {
270
+ // Returns [upper curve, connector, lower curve]
271
+ private currentSegmentToPath(): CurrentSegmentToPathResult {
164
272
  if (this.currentCurve == null) {
165
273
  throw new Error('Invalid State: currentCurve is null!');
166
274
  }
@@ -217,31 +325,33 @@ export default class FreehandLineBuilder implements ComponentBuilder {
217
325
  halfVec = halfVec.times(2);
218
326
  }
219
327
 
328
+ // Each starts at startPt ± startVec
220
329
 
221
- const pathCommands: PathCommand[] = [
222
- {
223
- kind: PathCommandType.QuadraticBezierTo,
224
- controlPoint: this.roundPoint(controlPoint.plus(halfVec)),
225
- endPoint: this.roundPoint(endPt.plus(endVec)),
226
- },
330
+ const lowerCurve: QuadraticBezierPathCommand = {
331
+ kind: PathCommandType.QuadraticBezierTo,
332
+ controlPoint: this.roundPoint(controlPoint.plus(halfVec)),
333
+ endPoint: this.roundPoint(endPt.plus(endVec)),
334
+ };
227
335
 
228
- {
229
- kind: PathCommandType.LineTo,
230
- point: this.roundPoint(endPt.minus(endVec)),
231
- },
232
-
233
- {
234
- kind: PathCommandType.QuadraticBezierTo,
235
- controlPoint: this.roundPoint(controlPoint.minus(halfVec)),
236
- endPoint: this.roundPoint(startPt.minus(startVec)),
237
- },
238
- ];
336
+ // From the end of the upperCurve to the start of the lowerCurve:
337
+ const upperToLowerConnector: LinePathCommand = {
338
+ kind: PathCommandType.LineTo,
339
+ point: this.roundPoint(startPt.plus(startVec)),
340
+ };
239
341
 
240
- return {
241
- startPoint: this.roundPoint(startPt.plus(startVec)),
242
- commands: pathCommands,
243
- style: this.getRenderingStyle(),
342
+ // From the end of lowerCurve to the start of upperCurve:
343
+ const lowerToUpperConnector: LinePathCommand = {
344
+ kind: PathCommandType.LineTo,
345
+ point: this.roundPoint(endPt.minus(endVec))
346
+ };
347
+
348
+ const upperCurve: QuadraticBezierPathCommand = {
349
+ kind: PathCommandType.QuadraticBezierTo,
350
+ controlPoint: this.roundPoint(controlPoint.minus(halfVec)),
351
+ endPoint: this.roundPoint(startPt.minus(startVec)),
244
352
  };
353
+
354
+ return { upperCurve, upperToLowerConnector, lowerToUpperConnector, lowerCurve };
245
355
  }
246
356
 
247
357
  // Compute the direction of the velocity at the end of this.buffer
@@ -264,7 +374,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
264
374
 
265
375
  const threshold = Math.min(this.lastPoint.width, newPoint.width) / 4;
266
376
  const shouldSnapToInitial = this.startPoint.pos.minus(newPoint.pos).magnitude() < threshold
267
- && this.segments.length === 0;
377
+ && this.isFirstSegment;
268
378
 
269
379
  // Snap to the starting point if the stroke is contained within a small ball centered
270
380
  // at the starting point.
@@ -1,5 +1,5 @@
1
- import { PathCommandType } from '../../geometry/Path';
2
- import Rect2 from '../../geometry/Rect2';
1
+ import { PathCommandType } from '../../math/Path';
2
+ import Rect2 from '../../math/Rect2';
3
3
  import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
4
4
  import { StrokeDataPoint } from '../../types';
5
5
  import Viewport from '../../Viewport';
@@ -1,6 +1,6 @@
1
- import Mat33 from '../../geometry/Mat33';
2
- import Path from '../../geometry/Path';
3
- import Rect2 from '../../geometry/Rect2';
1
+ import Mat33 from '../../math/Mat33';
2
+ import Path from '../../math/Path';
3
+ import Rect2 from '../../math/Rect2';
4
4
  import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
5
5
  import { StrokeDataPoint } from '../../types';
6
6
  import Viewport from '../../Viewport';
@@ -1,4 +1,4 @@
1
- import Rect2 from '../../geometry/Rect2';
1
+ import Rect2 from '../../math/Rect2';
2
2
  import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
3
3
  import { StrokeDataPoint } from '../../types';
4
4
  import Viewport from '../../Viewport';
@@ -6,6 +6,7 @@ import { defaultToolLocalization, ToolLocalization } from './tools/localization'
6
6
 
7
7
 
8
8
  export interface EditorLocalization extends ToolbarLocalization, ToolLocalization, CommandLocalization, ImageComponentLocalization, TextRendererLocalization {
9
+ accessibilityInputInstructions: string;
9
10
  undoAnnouncement: (actionDescription: string)=> string;
10
11
  redoAnnouncement: (actionDescription: string)=> string;
11
12
  doneLoading: string;
@@ -19,6 +20,11 @@ export const defaultEditorLocalization: EditorLocalization = {
19
20
  ...defaultCommandLocalization,
20
21
  ...defaultComponentLocalization,
21
22
  ...defaultTextRendererLocalization,
23
+ accessibilityInputInstructions: [
24
+ 'Press "t" to read the contents of the viewport as text.',
25
+ 'Use the arrow keys to move the viewport, click and drag to draw strokes.',
26
+ 'Press "w" to zoom in and "s" to zoom out.',
27
+ ].join(' '),
22
28
  loading: (percentage: number) => `Loading ${percentage}%...`,
23
29
  imageEditor: 'Image Editor',
24
30
  doneLoading: 'Done loading',
@@ -38,7 +38,8 @@ const localization: EditorLocalization = {
38
38
  resizeImageToSelection: 'Redimensionar la imagen a lo que está seleccionado',
39
39
  deleteSelection: 'Borra la selección',
40
40
  duplicateSelection: 'Duplica la selección',
41
- pickColorFronScreen: 'Selecciona un color de la pantalla',
41
+ pickColorFromScreen: 'Selecciona un color de la pantalla',
42
+ clickToPickColorAnnouncement: 'Haga un clic en la pantalla para seleccionar un color',
42
43
  dropdownShown(toolName: string): string {
43
44
  return `Menú por ${toolName} es visible`;
44
45
  },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -15,7 +15,7 @@ describe('Path.toString', () => {
15
15
  point: Vec2.of(0.3, 0.4),
16
16
  },
17
17
  ]);
18
- expect(path.toString()).toBe('M0.1,0.2L0.3,0.4');
18
+ expect(path.toString()).toBe('M.1,.2L.3,.4');
19
19
  });
20
20
 
21
21
  it('should fix rounding errors', () => {
@@ -30,7 +30,8 @@ describe('Path.toString', () => {
30
30
  point: Vec2.of(184.00482359999998, 1)
31
31
  }
32
32
  ]);
33
- expect(path.toString()).toBe('M0.1,0.2Q9999,-11 0.0003,1.4L184.0048236,1');
33
+
34
+ expect(path.toString()).toBe('M.1,.2Q9999,-11 .0003,1.4L184.0048236,1');
34
35
  });
35
36
 
36
37
  it('should not remove trailing zeroes before decimal points', () => {
@@ -40,6 +41,14 @@ describe('Path.toString', () => {
40
41
  point: Vec2.of(30.0001, 40.000000001),
41
42
  },
42
43
  ]);
44
+
43
45
  expect(path.toString()).toBe('M1000,2000000L30.0001,40');
44
46
  });
47
+
48
+ it('deserialized path should serialize to the same/similar path, but with rounded components', () => {
49
+ const path1 = Path.fromString('M100,100 L101,101 Q102,102 90.000000001,89.99999999 Z');
50
+ expect(path1.toString()).toBe([
51
+ 'M100,100', 'L101,101', 'Q102,102 90,90', 'L100,100'
52
+ ].join(''));
53
+ });
45
54
  });
@@ -1,6 +1,7 @@
1
1
  import { Bezier } from 'bezier-js';
2
2
  import { RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
3
3
  import RenderingStyle from '../rendering/RenderingStyle';
4
+ import { toRoundedString, toStringOfSamePrecision } from './rounding';
4
5
  import LineSegment2 from './LineSegment2';
5
6
  import Mat33 from './Mat33';
6
7
  import Rect2 from './Rect2';
@@ -170,8 +171,8 @@ export default class Path {
170
171
  return result;
171
172
  }
172
173
 
173
- public transformedBy(affineTransfm: Mat33): Path {
174
- const startPoint = affineTransfm.transformVec2(this.startPoint);
174
+ public mapPoints(mapping: (point: Point2)=>Point2): Path {
175
+ const startPoint = mapping(this.startPoint);
175
176
  const newParts: PathCommand[] = [];
176
177
 
177
178
  let exhaustivenessCheck: never;
@@ -181,22 +182,22 @@ export default class Path {
181
182
  case PathCommandType.LineTo:
182
183
  newParts.push({
183
184
  kind: part.kind,
184
- point: affineTransfm.transformVec2(part.point),
185
+ point: mapping(part.point),
185
186
  });
186
187
  break;
187
188
  case PathCommandType.CubicBezierTo:
188
189
  newParts.push({
189
190
  kind: part.kind,
190
- controlPoint1: affineTransfm.transformVec2(part.controlPoint1),
191
- controlPoint2: affineTransfm.transformVec2(part.controlPoint2),
192
- endPoint: affineTransfm.transformVec2(part.endPoint),
191
+ controlPoint1: mapping(part.controlPoint1),
192
+ controlPoint2: mapping(part.controlPoint2),
193
+ endPoint: mapping(part.endPoint),
193
194
  });
194
195
  break;
195
196
  case PathCommandType.QuadraticBezierTo:
196
197
  newParts.push({
197
198
  kind: part.kind,
198
- controlPoint: affineTransfm.transformVec2(part.controlPoint),
199
- endPoint: affineTransfm.transformVec2(part.endPoint),
199
+ controlPoint: mapping(part.controlPoint),
200
+ endPoint: mapping(part.endPoint),
200
201
  });
201
202
  break;
202
203
  default:
@@ -208,6 +209,10 @@ export default class Path {
208
209
  return new Path(startPoint, newParts);
209
210
  }
210
211
 
212
+ public transformedBy(affineTransfm: Mat33): Path {
213
+ return this.mapPoints(point => affineTransfm.transformVec2(point));
214
+ }
215
+
211
216
  // Creates a new path by joining [other] to the end of this path
212
217
  public union(other: Path|null): Path {
213
218
  if (!other) {
@@ -280,71 +285,69 @@ export default class Path {
280
285
  }
281
286
 
282
287
  public toString(): string {
283
- return Path.toString(this.startPoint, this.parts);
288
+ // Hueristic: Try to determine whether converting absolute to relative commands is worth it.
289
+ // If we're near (0, 0), it probably isn't worth it and if bounding boxes are large,
290
+ // it also probably isn't worth it.
291
+ const makeRelativeCommands =
292
+ Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.size.x) < 2
293
+ && Math.abs(this.bbox.topLeft.y) > 10 && Math.abs(this.bbox.size.y) < 2;
294
+
295
+ return Path.toString(this.startPoint, this.parts, !makeRelativeCommands);
284
296
  }
285
297
 
286
298
  public serialize(): string {
287
299
  return this.toString();
288
300
  }
289
301
 
290
- public static toString(startPoint: Point2, parts: PathCommand[]): string {
302
+ // [onlyAbsCommands]: True if we should avoid converting absolute coordinates to relative offsets -- such
303
+ // conversions can lead to smaller output strings, but also take time.
304
+ public static toString(startPoint: Point2, parts: PathCommand[], onlyAbsCommands: boolean = true): string {
291
305
  const result: string[] = [];
292
306
 
293
- const toRoundedString = (num: number): string => {
294
- // Try to remove rounding errors. If the number ends in at least three/four zeroes
295
- // (or nines) just one or two digits, it's probably a rounding error.
296
- const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d$/;
297
- const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,}\d)$/;
298
-
299
- let text = num.toString();
300
- if (text.indexOf('.') === -1) {
301
- return text;
302
- }
307
+ let prevPoint: Point2|undefined;
308
+ const addCommand = (command: string, ...points: Point2[]) => {
309
+ const absoluteCommandParts: string[] = [];
310
+ const relativeCommandParts: string[] = [];
311
+ const makeAbsCommand = !prevPoint || onlyAbsCommands;
312
+ const roundedPrevX = prevPoint ? toRoundedString(prevPoint.x) : '';
313
+ const roundedPrevY = prevPoint ? toRoundedString(prevPoint.y) : '';
303
314
 
304
- const roundingDownMatch = hasRoundingDownExp.exec(text);
305
- if (roundingDownMatch) {
306
- const negativeSign = roundingDownMatch[1];
307
- const lastDigit = parseInt(text.charAt(text.length - 1), 10);
308
- const postDecimal = parseInt(roundingDownMatch[3], 10);
309
- const preDecimal = parseInt(roundingDownMatch[2], 10);
310
-
311
- const origPostDecimalString = roundingDownMatch[3];
312
-
313
- let newPostDecimal = (postDecimal + 10 - lastDigit).toString();
314
- let carry = 0;
315
- if (newPostDecimal.length > postDecimal.toString().length) {
316
- // Left-shift
317
- newPostDecimal = newPostDecimal.substring(1);
318
- carry = 1;
319
- }
315
+ for (const point of points) {
316
+ // Relative commands are often shorter as strings than absolute commands.
317
+ if (!makeAbsCommand) {
318
+ const xComponentRelative = toStringOfSamePrecision(point.x - prevPoint!.x, roundedPrevX, roundedPrevY);
319
+ const yComponentRelative = toStringOfSamePrecision(point.y - prevPoint!.y, roundedPrevX, roundedPrevY);
320
+
321
+ // No need for an additional separator if it starts with a '-'
322
+ if (yComponentRelative.charAt(0) === '-') {
323
+ relativeCommandParts.push(`${xComponentRelative}${yComponentRelative}`);
324
+ } else {
325
+ relativeCommandParts.push(`${xComponentRelative},${yComponentRelative}`);
326
+ }
327
+ } else {
328
+ const xComponent = toRoundedString(point.x);
329
+ const yComponent = toRoundedString(point.y);
320
330
 
321
- // parseInt(...).toString() removes leading zeroes. Add them back.
322
- while (newPostDecimal.length < origPostDecimalString.length) {
323
- newPostDecimal = carry.toString(10) + newPostDecimal;
324
- carry = 0;
331
+ absoluteCommandParts.push(`${xComponent},${yComponent}`);
325
332
  }
326
-
327
- text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
328
333
  }
329
334
 
330
- text = text.replace(fixRoundingUpExp, '$1');
331
-
332
- // Remove trailing zeroes
333
- text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
334
- text = text.replace(/[.]0+$/, '.');
335
+ let commandString;
336
+ if (makeAbsCommand) {
337
+ commandString = `${command}${absoluteCommandParts.join(' ')}`;
338
+ } else {
339
+ commandString = `${command.toLowerCase()}${relativeCommandParts.join(' ')}`;
340
+ }
335
341
 
336
- // Remove trailing period
337
- return text.replace(/[.]$/, '');
338
- };
342
+ // Don't add no-ops.
343
+ if (commandString === 'l0,0') {
344
+ return;
345
+ }
346
+ result.push(commandString);
339
347
 
340
- const addCommand = (command: string, ...points: Point2[]) => {
341
- const parts: string[] = [];
342
- for (const point of points) {
343
- const xComponent = toRoundedString(point.x);
344
- const yComponent = toRoundedString(point.y);
345
- parts.push(`${xComponent},${yComponent}`);
348
+ if (points.length > 0) {
349
+ prevPoint = points[points.length - 1];
346
350
  }
347
- result.push(`${command}${parts.join(' ')}`);
348
351
  };
349
352
 
350
353
  addCommand('M', startPoint);
File without changes
File without changes
File without changes
File without changes
File without changes