js-draw 0.1.9 → 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 (193) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +15 -3
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +7 -2
  5. package/dist/src/Editor.js +74 -25
  6. package/dist/src/EditorImage.d.ts +1 -1
  7. package/dist/src/EditorImage.js +2 -2
  8. package/dist/src/Pointer.d.ts +1 -1
  9. package/dist/src/Pointer.js +1 -1
  10. package/dist/src/SVGLoader.d.ts +1 -1
  11. package/dist/src/SVGLoader.js +14 -6
  12. package/dist/src/UndoRedoHistory.js +3 -0
  13. package/dist/src/Viewport.d.ts +8 -25
  14. package/dist/src/Viewport.js +17 -10
  15. package/dist/src/bundle/bundled.d.ts +2 -1
  16. package/dist/src/bundle/bundled.js +2 -1
  17. package/dist/src/commands/Command.d.ts +2 -2
  18. package/dist/src/commands/Command.js +4 -4
  19. package/dist/src/commands/Duplicate.d.ts +1 -1
  20. package/dist/src/commands/Duplicate.js +1 -1
  21. package/dist/src/commands/Erase.d.ts +1 -1
  22. package/dist/src/commands/Erase.js +1 -1
  23. package/dist/src/commands/localization.d.ts +1 -1
  24. package/dist/src/components/AbstractComponent.d.ts +3 -3
  25. package/dist/src/components/AbstractComponent.js +2 -2
  26. package/dist/src/components/SVGGlobalAttributesObject.d.ts +3 -3
  27. package/dist/src/components/SVGGlobalAttributesObject.js +1 -1
  28. package/dist/src/components/Stroke.d.ts +4 -4
  29. package/dist/src/components/Stroke.js +2 -2
  30. package/dist/src/components/Text.d.ts +3 -3
  31. package/dist/src/components/Text.js +3 -3
  32. package/dist/src/components/UnknownSVGObject.d.ts +3 -3
  33. package/dist/src/components/UnknownSVGObject.js +1 -1
  34. package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
  35. package/dist/src/components/builders/ArrowBuilder.js +1 -1
  36. package/dist/src/components/builders/FreehandLineBuilder.d.ts +8 -3
  37. package/dist/src/components/builders/FreehandLineBuilder.js +142 -71
  38. package/dist/src/components/builders/LineBuilder.d.ts +1 -1
  39. package/dist/src/components/builders/LineBuilder.js +1 -1
  40. package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
  41. package/dist/src/components/builders/RectangleBuilder.js +3 -3
  42. package/dist/src/components/builders/types.d.ts +1 -1
  43. package/dist/src/localization.d.ts +1 -0
  44. package/dist/src/localization.js +5 -1
  45. package/dist/src/localizations/en.d.ts +3 -0
  46. package/dist/src/localizations/en.js +4 -0
  47. package/dist/src/localizations/es.d.ts +3 -0
  48. package/dist/src/localizations/es.js +18 -0
  49. package/dist/src/localizations/getLocalizationTable.d.ts +3 -0
  50. package/dist/src/localizations/getLocalizationTable.js +43 -0
  51. package/dist/src/{geometry → math}/LineSegment2.d.ts +1 -0
  52. package/dist/src/{geometry → math}/LineSegment2.js +16 -0
  53. package/dist/src/{geometry → math}/Mat33.d.ts +0 -0
  54. package/dist/src/{geometry → math}/Mat33.js +0 -0
  55. package/dist/src/{geometry → math}/Path.d.ts +2 -1
  56. package/dist/src/{geometry → math}/Path.js +58 -51
  57. package/dist/src/{geometry → math}/Rect2.d.ts +1 -0
  58. package/dist/src/{geometry → math}/Rect2.js +16 -0
  59. package/dist/src/{geometry → math}/Vec2.d.ts +0 -0
  60. package/dist/src/{geometry → math}/Vec2.js +0 -0
  61. package/dist/src/{geometry → math}/Vec3.d.ts +1 -1
  62. package/dist/src/{geometry → math}/Vec3.js +1 -1
  63. package/dist/src/math/rounding.d.ts +3 -0
  64. package/dist/src/math/rounding.js +120 -0
  65. package/dist/src/rendering/Display.d.ts +3 -1
  66. package/dist/src/rendering/Display.js +16 -10
  67. package/dist/src/rendering/caching/CacheRecord.d.ts +2 -2
  68. package/dist/src/rendering/caching/CacheRecord.js +1 -1
  69. package/dist/src/rendering/caching/CacheRecordManager.d.ts +1 -1
  70. package/dist/src/rendering/caching/RenderingCache.js +1 -1
  71. package/dist/src/rendering/caching/RenderingCacheNode.d.ts +2 -1
  72. package/dist/src/rendering/caching/RenderingCacheNode.js +18 -7
  73. package/dist/src/rendering/caching/testUtils.js +1 -1
  74. package/dist/src/rendering/caching/types.d.ts +1 -1
  75. package/dist/src/rendering/localization.d.ts +2 -0
  76. package/dist/src/rendering/localization.js +2 -0
  77. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +4 -4
  78. package/dist/src/rendering/renderers/AbstractRenderer.js +2 -2
  79. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +4 -4
  80. package/dist/src/rendering/renderers/CanvasRenderer.js +1 -1
  81. package/dist/src/rendering/renderers/DummyRenderer.d.ts +4 -4
  82. package/dist/src/rendering/renderers/DummyRenderer.js +1 -1
  83. package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -3
  84. package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
  85. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +5 -3
  86. package/dist/src/rendering/renderers/TextOnlyRenderer.js +13 -3
  87. package/dist/src/toolbar/HTMLToolbar.js +5 -7
  88. package/dist/src/toolbar/icons.d.ts +3 -0
  89. package/dist/src/toolbar/icons.js +152 -142
  90. package/dist/src/toolbar/localization.d.ts +4 -1
  91. package/dist/src/toolbar/localization.js +4 -1
  92. package/dist/src/toolbar/makeColorInput.js +2 -1
  93. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +13 -0
  94. package/dist/src/toolbar/widgets/ActionButtonWidget.js +21 -0
  95. package/dist/src/toolbar/widgets/BaseWidget.js +31 -0
  96. package/dist/src/toolbar/widgets/HandToolWidget.js +10 -3
  97. package/dist/src/toolbar/widgets/SelectionWidget.d.ts +0 -1
  98. package/dist/src/toolbar/widgets/SelectionWidget.js +23 -30
  99. package/dist/src/tools/BaseTool.d.ts +2 -1
  100. package/dist/src/tools/BaseTool.js +3 -0
  101. package/dist/src/tools/Eraser.js +1 -1
  102. package/dist/src/tools/PanZoom.d.ts +3 -2
  103. package/dist/src/tools/PanZoom.js +32 -16
  104. package/dist/src/tools/SelectionTool.d.ts +11 -4
  105. package/dist/src/tools/SelectionTool.js +135 -23
  106. package/dist/src/tools/TextTool.js +1 -1
  107. package/dist/src/tools/ToolController.js +6 -2
  108. package/dist/src/tools/localization.d.ts +1 -0
  109. package/dist/src/tools/localization.js +1 -0
  110. package/dist/src/types.d.ts +13 -6
  111. package/dist/src/types.js +1 -0
  112. package/package.json +9 -1
  113. package/src/Editor.ts +86 -24
  114. package/src/EditorImage.test.ts +2 -4
  115. package/src/EditorImage.ts +2 -2
  116. package/src/Pointer.ts +1 -1
  117. package/src/SVGLoader.ts +14 -6
  118. package/src/UndoRedoHistory.ts +4 -0
  119. package/src/Viewport.ts +21 -17
  120. package/src/bundle/bundled.ts +2 -1
  121. package/src/commands/Command.ts +5 -5
  122. package/src/commands/Duplicate.ts +1 -1
  123. package/src/commands/Erase.ts +1 -1
  124. package/src/commands/localization.ts +1 -1
  125. package/src/components/AbstractComponent.ts +4 -4
  126. package/src/components/SVGGlobalAttributesObject.ts +3 -3
  127. package/src/components/Stroke.test.ts +3 -5
  128. package/src/components/Stroke.ts +4 -4
  129. package/src/components/Text.test.ts +2 -2
  130. package/src/components/Text.ts +3 -3
  131. package/src/components/UnknownSVGObject.ts +3 -3
  132. package/src/components/builders/ArrowBuilder.ts +2 -2
  133. package/src/components/builders/FreehandLineBuilder.ts +190 -80
  134. package/src/components/builders/LineBuilder.ts +2 -2
  135. package/src/components/builders/RectangleBuilder.ts +3 -3
  136. package/src/components/builders/types.ts +1 -1
  137. package/src/localization.ts +6 -0
  138. package/src/localizations/en.ts +8 -0
  139. package/src/localizations/es.ts +63 -0
  140. package/src/localizations/getLocalizationTable.test.ts +27 -0
  141. package/src/localizations/getLocalizationTable.ts +53 -0
  142. package/src/{geometry → math}/LineSegment2.test.ts +15 -0
  143. package/src/{geometry → math}/LineSegment2.ts +20 -0
  144. package/src/{geometry → math}/Mat33.test.ts +0 -0
  145. package/src/{geometry → math}/Mat33.ts +0 -0
  146. package/src/{geometry → math}/Path.fromString.test.ts +0 -0
  147. package/src/{geometry → math}/Path.test.ts +0 -0
  148. package/src/{geometry → math}/Path.toString.test.ts +11 -2
  149. package/src/{geometry → math}/Path.ts +60 -57
  150. package/src/{geometry → math}/Rect2.test.ts +20 -7
  151. package/src/{geometry → math}/Rect2.ts +19 -1
  152. package/src/{geometry → math}/Vec2.test.ts +0 -0
  153. package/src/{geometry → math}/Vec2.ts +0 -0
  154. package/src/{geometry → math}/Vec3.test.ts +0 -0
  155. package/src/{geometry → math}/Vec3.ts +2 -2
  156. package/src/math/rounding.test.ts +40 -0
  157. package/src/math/rounding.ts +145 -0
  158. package/src/rendering/Display.ts +18 -10
  159. package/src/rendering/caching/CacheRecord.test.ts +2 -2
  160. package/src/rendering/caching/CacheRecord.ts +2 -2
  161. package/src/rendering/caching/CacheRecordManager.ts +1 -1
  162. package/src/rendering/caching/RenderingCache.test.ts +3 -3
  163. package/src/rendering/caching/RenderingCache.ts +1 -1
  164. package/src/rendering/caching/RenderingCacheNode.ts +23 -7
  165. package/src/rendering/caching/testUtils.ts +1 -1
  166. package/src/rendering/caching/types.ts +1 -1
  167. package/src/rendering/localization.ts +4 -0
  168. package/src/rendering/renderers/AbstractRenderer.ts +4 -4
  169. package/src/rendering/renderers/CanvasRenderer.ts +4 -4
  170. package/src/rendering/renderers/DummyRenderer.test.ts +2 -2
  171. package/src/rendering/renderers/DummyRenderer.ts +4 -4
  172. package/src/rendering/renderers/SVGRenderer.ts +10 -4
  173. package/src/rendering/renderers/TextOnlyRenderer.ts +17 -6
  174. package/src/toolbar/HTMLToolbar.ts +5 -8
  175. package/src/toolbar/icons.ts +167 -147
  176. package/src/toolbar/localization.ts +8 -2
  177. package/src/toolbar/makeColorInput.ts +2 -1
  178. package/src/toolbar/toolbar.css +7 -3
  179. package/src/toolbar/widgets/ActionButtonWidget.ts +31 -0
  180. package/src/toolbar/widgets/BaseWidget.ts +36 -0
  181. package/src/toolbar/widgets/HandToolWidget.ts +14 -3
  182. package/src/toolbar/widgets/SelectionWidget.ts +46 -41
  183. package/src/tools/BaseTool.ts +5 -1
  184. package/src/tools/Eraser.ts +2 -2
  185. package/src/tools/PanZoom.ts +39 -18
  186. package/src/tools/SelectionTool.test.ts +26 -5
  187. package/src/tools/SelectionTool.ts +162 -27
  188. package/src/tools/TextTool.ts +2 -2
  189. package/src/tools/ToolController.ts +6 -2
  190. package/src/tools/UndoRedoShortcut.test.ts +1 -1
  191. package/src/tools/localization.ts +2 -0
  192. package/src/types.ts +14 -5
  193. package/dist-test/test-dist-bundle.html +0 -42
@@ -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);
@@ -147,14 +147,27 @@ describe('Rect2', () => {
147
147
  it('division of empty square', () => {
148
148
  expect(Rect2.empty.divideIntoGrid(1000, 10000).length).toBe(1);
149
149
  });
150
+
151
+ it('division of rectangle', () => {
152
+ expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject(
153
+ [
154
+ new Rect2(0, 0, 1, 0.5), new Rect2(1, 0, 1, 0.5),
155
+ new Rect2(0, 0.5, 1, 0.5), new Rect2(1, 0.5, 1, 0.5),
156
+ ]
157
+ );
158
+ });
150
159
  });
151
160
 
152
- it('division of rectangle', () => {
153
- expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject(
154
- [
155
- new Rect2(0, 0, 1, 0.5), new Rect2(1, 0, 1, 0.5),
156
- new Rect2(0, 0.5, 1, 0.5), new Rect2(1, 0.5, 1, 0.5),
157
- ]
158
- );
161
+ describe('should correctly return the closest point on the edge of a rectangle', () => {
162
+ it('with the unit square', () => {
163
+ const rect = Rect2.unitSquare;
164
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.zero)).objEq(Vec2.zero);
165
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(-1, -1))).objEq(Vec2.zero);
166
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(-1, 0.5))).objEq(Vec2.of(0, 0.5));
167
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(1, 0.5))).objEq(Vec2.of(1, 0.5));
168
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(0.6, 0.6))).objEq(Vec2.of(1, 0.6));
169
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(2, 0.5))).objEq(Vec2.of(1, 0.5));
170
+ expect(rect.getClosestPointOnBoundaryTo(Vec2.of(0.6, 0.6))).objEq(Vec2.of(1, 0.6));
171
+ });
159
172
  });
160
173
  });
@@ -51,6 +51,7 @@ export default class Rect2 {
51
51
  return new Rect2(vec.x + this.x, vec.y + this.y, this.w, this.h);
52
52
  }
53
53
 
54
+ // Returns a copy of this with the given size (but same top-left).
54
55
  public resizedTo(size: Vec2): Rect2 {
55
56
  return new Rect2(this.x, this.y, size.x, size.y);
56
57
  }
@@ -163,6 +164,23 @@ export default class Rect2 {
163
164
  );
164
165
  }
165
166
 
167
+ public getClosestPointOnBoundaryTo(target: Point2) {
168
+ const closestEdgePoints = this.getEdges().map(edge => {
169
+ return edge.closestPointTo(target);
170
+ });
171
+
172
+ let closest: Point2|null = null;
173
+ let closestDist: number|null = null;
174
+ for (const point of closestEdgePoints) {
175
+ const dist = point.minus(target).length();
176
+ if (closestDist === null || dist < closestDist) {
177
+ closest = point;
178
+ closestDist = dist;
179
+ }
180
+ }
181
+ return closest!;
182
+ }
183
+
166
184
  public get corners(): Point2[] {
167
185
  return [
168
186
  this.bottomRight,
@@ -172,7 +190,7 @@ export default class Rect2 {
172
190
  ];
173
191
  }
174
192
 
175
- public get maxDimension(): number {
193
+ public get maxDimension() {
176
194
  return Math.max(this.w, this.h);
177
195
  }
178
196
 
File without changes
File without changes
File without changes
@@ -115,9 +115,9 @@ export default class Vec3 {
115
115
  }
116
116
 
117
117
  // Returns a vector with each component acted on by [fn]
118
- public map(fn: (component: number)=> number): Vec3 {
118
+ public map(fn: (component: number, index: number)=> number): Vec3 {
119
119
  return Vec3.of(
120
- fn(this.x), fn(this.y), fn(this.z)
120
+ fn(this.x, 0), fn(this.y, 1), fn(this.z, 2)
121
121
  );
122
122
  }
123
123
 
@@ -0,0 +1,40 @@
1
+ import { toRoundedString, toStringOfSamePrecision } from './rounding';
2
+
3
+ describe('toRoundedString', () => {
4
+ it('should round up numbers endings similar to .999999999999999', () => {
5
+ expect(toRoundedString(0.999999999)).toBe('1');
6
+ expect(toRoundedString(0.899999999)).toBe('.9');
7
+ expect(toRoundedString(9.999999999)).toBe('10');
8
+ expect(toRoundedString(-10.999999999)).toBe('-11');
9
+ });
10
+
11
+ it('should round up numbers similar to 10.999999998', () => {
12
+ expect(toRoundedString(10.999999998)).toBe('11');
13
+ });
14
+
15
+ // Handling this creates situations with potential error:
16
+ //it('should round strings with multiple digits after the ending decimal points', () => {
17
+ // expect(toRoundedString(292.2 - 292.8)).toBe('-0.6');
18
+ //});
19
+
20
+ it('should round down strings ending endings similar to .00000001', () => {
21
+ expect(toRoundedString(10.00000001)).toBe('10');
22
+ });
23
+ });
24
+
25
+ it('toStringOfSamePrecision', () => {
26
+ expect(toStringOfSamePrecision(1.23456, '1.12')).toBe('1.23');
27
+ expect(toStringOfSamePrecision(1.23456, '1.1')).toBe('1.2');
28
+ expect(toStringOfSamePrecision(1.23456, '1.1', '5.32')).toBe('1.23');
29
+ expect(toStringOfSamePrecision(-1.23456, '1.1', '5.32')).toBe('-1.23');
30
+ expect(toStringOfSamePrecision(-1.99999, '1.1', '5.32')).toBe('-2');
31
+ expect(toStringOfSamePrecision(1.99999, '1.1', '5.32')).toBe('2');
32
+ expect(toStringOfSamePrecision(1.89999, '1.1', '5.32')).toBe('1.9');
33
+ expect(toStringOfSamePrecision(9.99999999, '-1.1234')).toBe('10');
34
+ expect(toStringOfSamePrecision(9.999999998999996, '100')).toBe('10');
35
+ expect(toStringOfSamePrecision(0.000012345, '0.000012')).toBe('.000012');
36
+ expect(toStringOfSamePrecision(0.000012645, '.000012')).toBe('.000013');
37
+ expect(toStringOfSamePrecision(-0.09999999999999432, '291.3')).toBe('-.1');
38
+ expect(toStringOfSamePrecision(-0.9999999999999432, '291.3')).toBe('-1');
39
+ expect(toStringOfSamePrecision(9998.9, '.1', '-11')).toBe('9998.9');
40
+ });
@@ -0,0 +1,145 @@
1
+ // Clean up stringified numbers
2
+ const cleanUpNumber = (text: string) => {
3
+ // Regular expression substitions can be somewhat expensive. Only do them
4
+ // if necessary.
5
+ const lastChar = text.charAt(text.length - 1);
6
+ if (lastChar === '0' || lastChar === '.') {
7
+ // Remove trailing zeroes
8
+ text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
9
+ text = text.replace(/[.]0+$/, '.');
10
+
11
+ // Remove trailing period
12
+ text = text.replace(/[.]$/, '');
13
+
14
+ if (text === '-0') {
15
+ return '0';
16
+ }
17
+ }
18
+
19
+ const firstChar = text.charAt(0);
20
+ if (firstChar === '0' || firstChar === '-') {
21
+ // Remove unnecessary leading zeroes.
22
+ text = text.replace(/^(0+)[.]/, '.');
23
+ text = text.replace(/^-(0+)[.]/, '-.');
24
+ }
25
+
26
+ return text;
27
+ };
28
+
29
+ export const toRoundedString = (num: number): string => {
30
+ // Try to remove rounding errors. If the number ends in at least three/four zeroes
31
+ // (or nines) just one or two digits, it's probably a rounding error.
32
+ const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,2}$/;
33
+ const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,2}$/;
34
+
35
+ let text = num.toString(10);
36
+ if (text.indexOf('.') === -1) {
37
+ return text;
38
+ }
39
+
40
+ const roundingDownMatch = hasRoundingDownExp.exec(text);
41
+ if (roundingDownMatch) {
42
+ const negativeSign = roundingDownMatch[1];
43
+ const postDecimalString = roundingDownMatch[3];
44
+ const lastDigit = parseInt(postDecimalString.charAt(postDecimalString.length - 1), 10);
45
+ const postDecimal = parseInt(postDecimalString, 10);
46
+ const preDecimal = parseInt(roundingDownMatch[2], 10);
47
+
48
+ const origPostDecimalString = roundingDownMatch[3];
49
+
50
+ let newPostDecimal = (postDecimal + 10 - lastDigit).toString();
51
+ let carry = 0;
52
+ if (newPostDecimal.length > postDecimal.toString().length) {
53
+ // Left-shift
54
+ newPostDecimal = newPostDecimal.substring(1);
55
+ carry = 1;
56
+ }
57
+
58
+ // parseInt(...).toString() removes leading zeroes. Add them back.
59
+ while (newPostDecimal.length < origPostDecimalString.length) {
60
+ newPostDecimal = carry.toString(10) + newPostDecimal;
61
+ carry = 0;
62
+ }
63
+
64
+ text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
65
+ }
66
+
67
+ text = text.replace(fixRoundingUpExp, '$1');
68
+
69
+ return cleanUpNumber(text);
70
+ };
71
+
72
+ const numberExp = /^([-]?)(\d*)[.](\d+)$/;
73
+ export const getLenAfterDecimal = (numberAsString: string) => {
74
+ const numberMatch = numberExp.exec(numberAsString);
75
+ if (!numberMatch) {
76
+ // If not a match, either the number is exponential notation (or is something
77
+ // like NaN or Infinity)
78
+ if (numberAsString.search(/[eE]/) !== -1 || /^[a-zA-Z]+$/.exec(numberAsString)) {
79
+ return -1;
80
+ // Or it has no decimal point
81
+ } else {
82
+ return 0;
83
+ }
84
+ }
85
+
86
+ const afterDecimalLen = numberMatch[3].length;
87
+ return afterDecimalLen;
88
+ };
89
+
90
+ // [reference] should be a string representation of a base-10 number (no exponential (e.g. 10e10))
91
+ export const toStringOfSamePrecision = (num: number, ...references: string[]): string => {
92
+ const text = num.toString(10);
93
+ const textMatch = numberExp.exec(text);
94
+ if (!textMatch) {
95
+ return text;
96
+ }
97
+
98
+ let decimalPlaces = -1;
99
+ for (const reference of references) {
100
+ decimalPlaces = Math.max(getLenAfterDecimal(reference), decimalPlaces);
101
+ }
102
+
103
+ if (decimalPlaces === -1) {
104
+ return toRoundedString(num);
105
+ }
106
+
107
+ // Make text's after decimal length match [afterDecimalLen].
108
+ let postDecimal = textMatch[3].substring(0, decimalPlaces);
109
+ let preDecimal = textMatch[2];
110
+ const nextDigit = textMatch[3].charAt(decimalPlaces);
111
+
112
+ if (nextDigit !== '') {
113
+ const asNumber = parseInt(nextDigit, 10);
114
+ if (asNumber >= 5) {
115
+ // Don't attempt to parseInt() an empty string.
116
+ if (postDecimal.length > 0) {
117
+ const leadingZeroMatch = /^(0+)(\d*)$/.exec(postDecimal);
118
+
119
+ let leadingZeroes = '';
120
+ let postLeading = postDecimal;
121
+ if (leadingZeroMatch) {
122
+ leadingZeroes = leadingZeroMatch[1];
123
+ postLeading = leadingZeroMatch[2];
124
+ }
125
+
126
+ postDecimal = (parseInt(postDecimal) + 1).toString();
127
+
128
+ // If postDecimal got longer, remove leading zeroes if possible
129
+ if (postDecimal.length > postLeading.length && leadingZeroes.length > 0) {
130
+ leadingZeroes = leadingZeroes.substring(1);
131
+ }
132
+
133
+ postDecimal = leadingZeroes + postDecimal;
134
+ }
135
+
136
+ if (postDecimal.length === 0 || postDecimal.length > decimalPlaces) {
137
+ preDecimal = (parseInt(preDecimal) + 1).toString();
138
+ postDecimal = postDecimal.substring(1);
139
+ }
140
+ }
141
+ }
142
+
143
+ const negativeSign = textMatch[1];
144
+ return cleanUpNumber(`${negativeSign}${preDecimal}.${postDecimal}`);
145
+ };
@@ -3,7 +3,7 @@ import CanvasRenderer from './renderers/CanvasRenderer';
3
3
  import { Editor } from '../Editor';
4
4
  import { EditorEventType } from '../types';
5
5
  import DummyRenderer from './renderers/DummyRenderer';
6
- import { Point2, Vec2 } from '../geometry/Vec2';
6
+ import { Point2, Vec2 } from '../math/Vec2';
7
7
  import RenderingCache from './caching/RenderingCache';
8
8
  import TextOnlyRenderer from './renderers/TextOnlyRenderer';
9
9
  import Color4 from '../Color4';
@@ -18,6 +18,7 @@ export default class Display {
18
18
  private dryInkRenderer: AbstractRenderer;
19
19
  private wetInkRenderer: AbstractRenderer;
20
20
  private textRenderer: TextOnlyRenderer;
21
+ private textRerenderOutput: HTMLElement|null = null;
21
22
  private cache: RenderingCache;
22
23
  private resizeSurfacesCallback?: ()=> void;
23
24
  private flattenCallback?: ()=> void;
@@ -59,10 +60,10 @@ export default class Display {
59
60
  return this.dryInkRenderer.canRenderFromWithoutDataLoss(renderer);
60
61
  },
61
62
  blockResolution: cacheBlockResolution,
62
- cacheSize: 500 * 500 * 4 * 200,
63
+ cacheSize: 500 * 500 * 4 * 220,
63
64
  maxScale: 1.5,
64
- minComponentsPerCache: 50,
65
- minComponentsToUseCache: 120,
65
+ minComponentsPerCache: 45,
66
+ minComponentsToUseCache: 105,
66
67
  });
67
68
 
68
69
  this.editor.notifier.on(EditorEventType.DisplayResized, event => {
@@ -158,19 +159,26 @@ export default class Display {
158
159
  rerenderButton.classList.add('rerenderButton');
159
160
  rerenderButton.innerText = this.editor.localization.rerenderAsText;
160
161
 
161
- const rerenderOutput = document.createElement('div');
162
- rerenderOutput.ariaLive = 'polite';
162
+ this.textRerenderOutput = document.createElement('div');
163
+ this.textRerenderOutput.setAttribute('aria-live', 'polite');
163
164
 
164
165
  rerenderButton.onclick = () => {
165
- this.textRenderer.clear();
166
- this.editor.image.render(this.textRenderer, this.editor.viewport);
167
- rerenderOutput.innerText = this.textRenderer.getDescription();
166
+ this.rerenderAsText();
168
167
  };
169
168
 
170
- textRendererOutputContainer.replaceChildren(rerenderButton, rerenderOutput);
169
+ textRendererOutputContainer.replaceChildren(rerenderButton, this.textRerenderOutput);
171
170
  this.editor.createHTMLOverlay(textRendererOutputContainer);
172
171
  }
173
172
 
173
+ public rerenderAsText() {
174
+ this.textRenderer.clear();
175
+ this.editor.image.render(this.textRenderer, this.editor.viewport);
176
+
177
+ if (this.textRerenderOutput) {
178
+ this.textRerenderOutput.innerText = this.textRenderer.getDescription();
179
+ }
180
+ }
181
+
174
182
  // Clears the drawing surfaces and otherwise prepares for a rerender.
175
183
  public startRerender(): AbstractRenderer {
176
184
  this.resizeSurfacesCallback?.();
@@ -1,7 +1,7 @@
1
1
  /* @jest-environment jsdom */
2
2
 
3
- import Rect2 from '../../geometry/Rect2';
4
- import { Vec2 } from '../../geometry/Vec2';
3
+ import Rect2 from '../../math/Rect2';
4
+ import { Vec2 } from '../../math/Vec2';
5
5
  import CacheRecord from './CacheRecord';
6
6
  import { createCache } from './testUtils';
7
7
 
@@ -1,5 +1,5 @@
1
- import Mat33 from '../../geometry/Mat33';
2
- import Rect2 from '../../geometry/Rect2';
1
+ import Mat33 from '../../math/Mat33';
2
+ import Rect2 from '../../math/Rect2';
3
3
  import AbstractRenderer from '../renderers/AbstractRenderer';
4
4
  import { BeforeDeallocCallback, CacheState } from './types';
5
5
 
@@ -1,6 +1,6 @@
1
1
  import { BeforeDeallocCallback, PartialCacheState } from './types';
2
2
  import CacheRecord from './CacheRecord';
3
- import Rect2 from '../../geometry/Rect2';
3
+ import Rect2 from '../../math/Rect2';
4
4
 
5
5
 
6
6
  export class CacheRecordManager {
@@ -3,11 +3,11 @@
3
3
  import DummyRenderer from '../renderers/DummyRenderer';
4
4
  import { createCache } from './testUtils';
5
5
  import Stroke from '../../components/Stroke';
6
- import Path from '../../geometry/Path';
6
+ import Path from '../../math/Path';
7
7
  import Color4 from '../../Color4';
8
8
  import EditorImage from '../../EditorImage';
9
9
  import Viewport from '../../Viewport';
10
- import Mat33 from '../../geometry/Mat33';
10
+ import Mat33 from '../../math/Mat33';
11
11
 
12
12
  describe('RenderingCache', () => {
13
13
  const testPath = Path.fromString('M0,0 l100,500 l-20,20 L-100,-100');
@@ -35,7 +35,7 @@ describe('RenderingCache', () => {
35
35
  expect(lastRenderer).not.toBeNull();
36
36
  expect(lastRenderer!.renderedPathCount).toBe(1);
37
37
 
38
- editor.dispatch(new Viewport.ViewportTransform(Mat33.scaling2D(0.1)));
38
+ editor.dispatch(Viewport.transformBy(Mat33.scaling2D(0.1)));
39
39
  editor.image.renderWithCache(screenRenderer, cache, editor.viewport);
40
40
  expect(allocdRenderers).toBe(1);
41
41
  expect(lastRenderer!.renderedPathCount).toBe(1);
@@ -1,5 +1,5 @@
1
1
  import { ImageNode } from '../../EditorImage';
2
- import Rect2 from '../../geometry/Rect2';
2
+ import Rect2 from '../../math/Rect2';
3
3
  import Viewport from '../../Viewport';
4
4
  import AbstractRenderer from '../renderers/AbstractRenderer';
5
5
  import RenderingCacheNode from './RenderingCacheNode';