js-draw 0.1.11 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (220) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.firebaserc +5 -0
  3. package/.github/workflows/firebase-hosting-merge.yml +25 -0
  4. package/.github/workflows/firebase-hosting-pull-request.yml +22 -0
  5. package/.github/workflows/github-pages.yml +52 -0
  6. package/CHANGELOG.md +13 -0
  7. package/README.md +11 -6
  8. package/dist/bundle.js +1 -1
  9. package/dist/src/Color4.d.ts +19 -0
  10. package/dist/src/Color4.js +24 -3
  11. package/dist/src/Editor.d.ts +133 -4
  12. package/dist/src/Editor.js +124 -27
  13. package/dist/src/EditorImage.d.ts +8 -3
  14. package/dist/src/EditorImage.js +42 -26
  15. package/dist/src/EventDispatcher.d.ts +18 -0
  16. package/dist/src/EventDispatcher.js +19 -4
  17. package/dist/src/Pointer.d.ts +1 -1
  18. package/dist/src/Pointer.js +4 -3
  19. package/dist/src/SVGLoader.d.ts +1 -1
  20. package/dist/src/SVGLoader.js +14 -6
  21. package/dist/src/UndoRedoHistory.js +15 -2
  22. package/dist/src/Viewport.d.ts +8 -25
  23. package/dist/src/Viewport.js +18 -10
  24. package/dist/src/bundle/bundled.d.ts +1 -2
  25. package/dist/src/bundle/bundled.js +1 -2
  26. package/dist/src/commands/Command.d.ts +2 -2
  27. package/dist/src/commands/Command.js +4 -4
  28. package/dist/src/commands/Duplicate.d.ts +2 -2
  29. package/dist/src/commands/Duplicate.js +4 -5
  30. package/dist/src/commands/Erase.d.ts +2 -2
  31. package/dist/src/commands/Erase.js +7 -6
  32. package/dist/src/commands/SerializableCommand.d.ts +4 -5
  33. package/dist/src/commands/SerializableCommand.js +12 -4
  34. package/dist/src/commands/invertCommand.d.ts +4 -0
  35. package/dist/src/commands/invertCommand.js +44 -0
  36. package/dist/src/commands/lib.d.ts +6 -0
  37. package/dist/src/commands/lib.js +6 -0
  38. package/dist/src/commands/localization.d.ts +2 -1
  39. package/dist/src/commands/localization.js +1 -0
  40. package/dist/src/components/AbstractComponent.d.ts +16 -11
  41. package/dist/src/components/AbstractComponent.js +28 -17
  42. package/dist/src/components/SVGGlobalAttributesObject.d.ts +4 -4
  43. package/dist/src/components/SVGGlobalAttributesObject.js +8 -2
  44. package/dist/src/components/Stroke.d.ts +16 -6
  45. package/dist/src/components/Stroke.js +12 -9
  46. package/dist/src/components/Text.d.ts +5 -5
  47. package/dist/src/components/Text.js +9 -9
  48. package/dist/src/components/UnknownSVGObject.d.ts +4 -4
  49. package/dist/src/components/UnknownSVGObject.js +7 -2
  50. package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
  51. package/dist/src/components/builders/ArrowBuilder.js +1 -1
  52. package/dist/src/components/builders/FreehandLineBuilder.d.ts +8 -3
  53. package/dist/src/components/builders/FreehandLineBuilder.js +142 -71
  54. package/dist/src/components/builders/LineBuilder.d.ts +1 -1
  55. package/dist/src/components/builders/LineBuilder.js +1 -1
  56. package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
  57. package/dist/src/components/builders/RectangleBuilder.js +3 -3
  58. package/dist/src/components/builders/types.d.ts +1 -1
  59. package/dist/src/components/lib.d.ts +4 -0
  60. package/dist/src/components/lib.js +4 -0
  61. package/dist/src/lib.d.ts +25 -0
  62. package/dist/src/lib.js +25 -0
  63. package/dist/src/localization.d.ts +1 -0
  64. package/dist/src/localization.js +5 -1
  65. package/dist/src/localizations/es.js +1 -1
  66. package/dist/src/{geometry → math}/LineSegment2.d.ts +0 -0
  67. package/dist/src/{geometry → math}/LineSegment2.js +0 -0
  68. package/dist/src/math/Mat33.d.ts +78 -0
  69. package/dist/src/{geometry → math}/Mat33.js +48 -20
  70. package/dist/src/{geometry → math}/Path.d.ts +2 -1
  71. package/dist/src/{geometry → math}/Path.js +59 -52
  72. package/dist/src/{geometry → math}/Rect2.d.ts +2 -2
  73. package/dist/src/{geometry → math}/Rect2.js +0 -0
  74. package/dist/src/{geometry → math}/Vec2.d.ts +0 -0
  75. package/dist/src/{geometry → math}/Vec2.js +0 -0
  76. package/dist/src/math/Vec3.d.ts +96 -0
  77. package/dist/src/{geometry → math}/Vec3.js +63 -15
  78. package/dist/src/math/lib.d.ts +7 -0
  79. package/dist/src/math/lib.js +7 -0
  80. package/dist/src/math/rounding.d.ts +3 -0
  81. package/dist/src/math/rounding.js +121 -0
  82. package/dist/src/rendering/Display.d.ts +47 -1
  83. package/dist/src/rendering/Display.js +60 -15
  84. package/dist/src/rendering/caching/CacheRecord.d.ts +3 -2
  85. package/dist/src/rendering/caching/CacheRecord.js +4 -1
  86. package/dist/src/rendering/caching/CacheRecordManager.d.ts +5 -4
  87. package/dist/src/rendering/caching/CacheRecordManager.js +16 -4
  88. package/dist/src/rendering/caching/RenderingCache.d.ts +2 -3
  89. package/dist/src/rendering/caching/RenderingCache.js +10 -11
  90. package/dist/src/rendering/caching/RenderingCacheNode.d.ts +2 -1
  91. package/dist/src/rendering/caching/RenderingCacheNode.js +18 -7
  92. package/dist/src/rendering/caching/testUtils.js +1 -1
  93. package/dist/src/rendering/caching/types.d.ts +2 -4
  94. package/dist/src/rendering/localization.d.ts +2 -0
  95. package/dist/src/rendering/localization.js +2 -0
  96. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +4 -4
  97. package/dist/src/rendering/renderers/AbstractRenderer.js +2 -2
  98. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +4 -4
  99. package/dist/src/rendering/renderers/CanvasRenderer.js +2 -2
  100. package/dist/src/rendering/renderers/DummyRenderer.d.ts +4 -4
  101. package/dist/src/rendering/renderers/DummyRenderer.js +1 -1
  102. package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -3
  103. package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
  104. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +5 -3
  105. package/dist/src/rendering/renderers/TextOnlyRenderer.js +13 -3
  106. package/dist/src/toolbar/HTMLToolbar.js +1 -0
  107. package/dist/src/toolbar/icons.d.ts +3 -0
  108. package/dist/src/toolbar/icons.js +142 -132
  109. package/dist/src/toolbar/localization.d.ts +2 -1
  110. package/dist/src/toolbar/localization.js +2 -1
  111. package/dist/src/toolbar/makeColorInput.js +3 -2
  112. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +13 -0
  113. package/dist/src/toolbar/widgets/ActionButtonWidget.js +21 -0
  114. package/dist/src/toolbar/widgets/BaseWidget.js +2 -0
  115. package/dist/src/toolbar/widgets/HandToolWidget.js +3 -3
  116. package/dist/src/toolbar/widgets/PenWidget.js +1 -0
  117. package/dist/src/toolbar/widgets/SelectionWidget.d.ts +0 -1
  118. package/dist/src/toolbar/widgets/SelectionWidget.js +23 -30
  119. package/dist/src/tools/Eraser.js +1 -1
  120. package/dist/src/tools/PanZoom.d.ts +1 -1
  121. package/dist/src/tools/PanZoom.js +24 -14
  122. package/dist/src/tools/Pen.d.ts +1 -2
  123. package/dist/src/tools/Pen.js +8 -1
  124. package/dist/src/tools/PipetteTool.js +1 -0
  125. package/dist/src/tools/SelectionTool.d.ts +3 -3
  126. package/dist/src/tools/SelectionTool.js +51 -28
  127. package/dist/src/tools/TextTool.js +1 -1
  128. package/dist/src/types.d.ts +21 -10
  129. package/dist/src/types.js +7 -5
  130. package/firebase.json +16 -0
  131. package/package.json +118 -101
  132. package/src/Color4.ts +23 -2
  133. package/src/Editor.ts +181 -37
  134. package/src/EditorImage.test.ts +2 -4
  135. package/src/EditorImage.ts +46 -28
  136. package/src/EventDispatcher.ts +21 -6
  137. package/src/Pointer.ts +4 -3
  138. package/src/SVGLoader.ts +14 -6
  139. package/src/UndoRedoHistory.ts +18 -2
  140. package/src/Viewport.ts +23 -18
  141. package/src/bundle/bundled.ts +1 -2
  142. package/src/commands/Command.ts +5 -5
  143. package/src/commands/Duplicate.ts +4 -5
  144. package/src/commands/Erase.ts +7 -6
  145. package/src/commands/SerializableCommand.ts +17 -9
  146. package/src/commands/invertCommand.ts +51 -0
  147. package/src/commands/lib.ts +14 -0
  148. package/src/commands/localization.ts +3 -1
  149. package/src/components/AbstractComponent.ts +35 -24
  150. package/src/components/SVGGlobalAttributesObject.ts +11 -4
  151. package/src/components/Stroke.test.ts +4 -6
  152. package/src/components/Stroke.ts +15 -11
  153. package/src/components/Text.test.ts +2 -2
  154. package/src/components/Text.ts +9 -10
  155. package/src/components/UnknownSVGObject.ts +10 -4
  156. package/src/components/builders/ArrowBuilder.ts +2 -2
  157. package/src/components/builders/FreehandLineBuilder.ts +190 -80
  158. package/src/components/builders/LineBuilder.ts +2 -2
  159. package/src/components/builders/RectangleBuilder.ts +3 -3
  160. package/src/components/builders/types.ts +1 -1
  161. package/src/components/lib.ts +9 -0
  162. package/src/lib.ts +28 -0
  163. package/src/localization.ts +6 -0
  164. package/src/localizations/es.ts +2 -1
  165. package/src/{geometry → math}/LineSegment2.test.ts +0 -0
  166. package/src/{geometry → math}/LineSegment2.ts +0 -0
  167. package/src/{geometry → math}/Mat33.test.ts +0 -0
  168. package/src/{geometry → math}/Mat33.ts +48 -20
  169. package/src/{geometry → math}/Path.fromString.test.ts +0 -0
  170. package/src/{geometry → math}/Path.test.ts +0 -0
  171. package/src/{geometry → math}/Path.toString.test.ts +11 -2
  172. package/src/{geometry → math}/Path.ts +61 -58
  173. package/src/{geometry → math}/Rect2.test.ts +0 -0
  174. package/src/{geometry → math}/Rect2.ts +2 -2
  175. package/src/{geometry → math}/Vec2.test.ts +0 -0
  176. package/src/{geometry → math}/Vec2.ts +0 -0
  177. package/src/{geometry → math}/Vec3.test.ts +0 -0
  178. package/src/{geometry → math}/Vec3.ts +64 -16
  179. package/src/math/lib.ts +15 -0
  180. package/src/math/rounding.test.ts +40 -0
  181. package/src/math/rounding.ts +147 -0
  182. package/src/rendering/Display.ts +63 -15
  183. package/src/rendering/caching/CacheRecord.test.ts +3 -3
  184. package/src/rendering/caching/CacheRecord.ts +6 -2
  185. package/src/rendering/caching/CacheRecordManager.ts +34 -8
  186. package/src/rendering/caching/RenderingCache.test.ts +3 -3
  187. package/src/rendering/caching/RenderingCache.ts +11 -16
  188. package/src/rendering/caching/RenderingCacheNode.ts +23 -7
  189. package/src/rendering/caching/testUtils.ts +1 -1
  190. package/src/rendering/caching/types.ts +2 -7
  191. package/src/rendering/localization.ts +4 -0
  192. package/src/rendering/renderers/AbstractRenderer.ts +4 -4
  193. package/src/rendering/renderers/CanvasRenderer.ts +5 -5
  194. package/src/rendering/renderers/DummyRenderer.test.ts +2 -2
  195. package/src/rendering/renderers/DummyRenderer.ts +4 -4
  196. package/src/rendering/renderers/SVGRenderer.ts +10 -4
  197. package/src/rendering/renderers/TextOnlyRenderer.ts +17 -6
  198. package/src/toolbar/HTMLToolbar.ts +1 -0
  199. package/src/toolbar/icons.ts +157 -137
  200. package/src/toolbar/localization.ts +4 -2
  201. package/src/toolbar/makeColorInput.ts +3 -2
  202. package/src/toolbar/toolbar.css +1 -1
  203. package/src/toolbar/widgets/ActionButtonWidget.ts +31 -0
  204. package/src/toolbar/widgets/BaseWidget.ts +2 -0
  205. package/src/toolbar/widgets/HandToolWidget.ts +3 -3
  206. package/src/toolbar/widgets/PenWidget.ts +2 -0
  207. package/src/toolbar/widgets/SelectionWidget.ts +46 -41
  208. package/src/tools/Eraser.ts +2 -2
  209. package/src/tools/PanZoom.ts +28 -17
  210. package/src/tools/Pen.ts +11 -2
  211. package/src/tools/PipetteTool.ts +2 -0
  212. package/src/tools/SelectionTool.test.ts +2 -4
  213. package/src/tools/SelectionTool.ts +52 -24
  214. package/src/tools/TextTool.ts +2 -2
  215. package/src/tools/UndoRedoShortcut.test.ts +1 -1
  216. package/src/types.ts +23 -7
  217. package/tsconfig.json +4 -1
  218. package/typedoc.json +20 -0
  219. package/dist/src/geometry/Mat33.d.ts +0 -32
  220. package/dist/src/geometry/Vec3.d.ts +0 -34
@@ -1,15 +1,22 @@
1
1
  import { Point2, Vec2 } from './Vec2';
2
2
  import Vec3 from './Vec3';
3
3
 
4
- // Represents a three dimensional linear transformation or
5
- // a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
6
- // **and** translates while a linear transformation just scales/rotates/shears).
4
+ /**
5
+ * Represents a three dimensional linear transformation or
6
+ * a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
7
+ * **and** translates while a linear transformation just scales/rotates/shears).
8
+ */
7
9
  export default class Mat33 {
8
10
  private readonly rows: Vec3[];
9
11
 
10
- // ⎡ a1 a2 a3 ⎤
11
- // b1 b2 b3
12
- // ⎣ c1 c2 c3 ⎦
12
+ /**
13
+ * Creates a matrix from inputs in the form,
14
+ * ```
15
+ * ⎡ a1 a2 a3 ⎤
16
+ * ⎢ b1 b2 b3 ⎥
17
+ * ⎣ c1 c2 c3 ⎦
18
+ * ```
19
+ */
13
20
  public constructor(
14
21
  public readonly a1: number,
15
22
  public readonly a2: number,
@@ -30,6 +37,14 @@ export default class Mat33 {
30
37
  ];
31
38
  }
32
39
 
40
+ /**
41
+ * Creates a matrix from the given rows:
42
+ * ```
43
+ * ⎡ r1.x r1.y r1.z ⎤
44
+ * ⎢ r2.x r2.y r2.z ⎥
45
+ * ⎣ r3.x r3.y r3.z ⎦
46
+ * ```
47
+ */
33
48
  public static ofRows(r1: Vec3, r2: Vec3, r3: Vec3): Mat33 {
34
49
  return new Mat33(
35
50
  r1.x, r1.y, r1.z,
@@ -44,8 +59,13 @@ export default class Mat33 {
44
59
  0, 0, 1
45
60
  );
46
61
 
47
- // Either returns the inverse of this, or, if this matrix is singular/uninvertable,
48
- // returns Mat33.identity.
62
+ /**
63
+ * Either returns the inverse of this, or, if this matrix is singular/uninvertable,
64
+ * returns Mat33.identity.
65
+ *
66
+ * This may cache the computed inverse and return the cached version instead of recomputing
67
+ * it.
68
+ */
49
69
  public inverse(): Mat33 {
50
70
  return this.computeInverse() ?? Mat33.identity;
51
71
  }
@@ -162,9 +182,11 @@ export default class Mat33 {
162
182
  );
163
183
  }
164
184
 
165
- // Applies this as an affine transformation to the given vector.
166
- // Returns a transformed version of [other].
167
- public transformVec2(other: Vec3): Vec2 {
185
+ /**
186
+ * Applies this as an affine transformation to the given vector.
187
+ * Returns a transformed version of `other`.
188
+ */
189
+ public transformVec2(other: Vec2): Vec2 {
168
190
  // When transforming a Vec2, we want to use the z transformation
169
191
  // components of this for translation:
170
192
  // ⎡ . . tX ⎤
@@ -179,8 +201,10 @@ export default class Mat33 {
179
201
  return Vec2.of(intermediate.x, intermediate.y);
180
202
  }
181
203
 
182
- // Applies this as a linear transformation to the given vector (doesn't translate).
183
- // This is the standard way of transforming vectors in ℝ³.
204
+ /**
205
+ * Applies this as a linear transformation to the given vector (doesn't translate).
206
+ * This is the standard way of transforming vectors in ℝ³.
207
+ */
184
208
  public transformVec3(other: Vec3): Vec3 {
185
209
  return Vec3.of(
186
210
  this.rows[0].dot(other),
@@ -189,7 +213,7 @@ export default class Mat33 {
189
213
  );
190
214
  }
191
215
 
192
- // Returns true iff this = other ± fuzz
216
+ /** Returns true iff this = other ± fuzz */
193
217
  public eq(other: Mat33, fuzz: number = 0): boolean {
194
218
  for (let i = 0; i < 3; i++) {
195
219
  if (!this.rows[i].eq(other.rows[i], fuzz)) {
@@ -205,12 +229,16 @@ export default class Mat33 {
205
229
  ⎡ ${this.a1},\t ${this.a2},\t ${this.a3}\t ⎤
206
230
  ⎢ ${this.b1},\t ${this.b2},\t ${this.b3}\t ⎥
207
231
  ⎣ ${this.c1},\t ${this.c2},\t ${this.c3}\t ⎦
208
- `.trimRight();
232
+ `.trimEnd().trimStart();
209
233
  }
210
234
 
211
- // result[0] = top left element
212
- // result[1] = element at row zero, column 1
213
- // ...
235
+ /**
236
+ * ```
237
+ * result[0] = top left element
238
+ * result[1] = element at row zero, column 1
239
+ * ...
240
+ * ```
241
+ */
214
242
  public toArray(): number[] {
215
243
  return [
216
244
  this.a1, this.a2, this.a3,
@@ -219,7 +247,7 @@ export default class Mat33 {
219
247
  ];
220
248
  }
221
249
 
222
- // Constructs a 3x3 translation matrix (for translating Vec2s)
250
+ /** Constructs a 3x3 translation matrix (for translating `Vec2`s) */
223
251
  public static translation(amount: Vec2): Mat33 {
224
252
  // When transforming Vec2s by a 3x3 matrix, we give the input
225
253
  // Vec2s z = 1. As such,
@@ -269,7 +297,7 @@ export default class Mat33 {
269
297
  return result.rightMul(Mat33.translation(center.times(-1)));
270
298
  }
271
299
 
272
- // Converts a CSS-form matrix(a, b, c, d, e, f) to a Mat33.
300
+ /** Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33. */
273
301
  public static fromCSSMatrix(cssString: string): Mat33 {
274
302
  if (cssString === '' || cssString === 'none') {
275
303
  return Mat33.identity;
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
+ // @param 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);
@@ -374,7 +377,7 @@ export default class Path {
374
377
 
375
378
  // Create a Path from a SVG path specification.
376
379
  // TODO: Support a larger subset of SVG paths.
377
- // TODO: Support s,t shorthands.
380
+ // TODO: Support `s`,`t` commands shorthands.
378
381
  public static fromString(pathString: string): Path {
379
382
  // See the MDN reference:
380
383
  // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
File without changes
@@ -2,8 +2,8 @@ import LineSegment2 from './LineSegment2';
2
2
  import Mat33 from './Mat33';
3
3
  import { Point2, Vec2 } from './Vec2';
4
4
 
5
- // An object that can be converted to a Rect2.
6
- interface RectTemplate {
5
+ /** An object that can be converted to a Rect2. */
6
+ export interface RectTemplate {
7
7
  x: number;
8
8
  y: number;
9
9
  w?: number;
File without changes
File without changes
File without changes
@@ -1,6 +1,10 @@
1
1
 
2
2
 
3
- // A vector with three components. Can also be used to represent a two-component vector
3
+ /**
4
+ * A vector with three components. Can also be used to represent a two-component vector.
5
+ *
6
+ * A `Vec3` is immutable.
7
+ */
4
8
  export default class Vec3 {
5
9
  private constructor(
6
10
  public readonly x: number,
@@ -9,7 +13,7 @@ export default class Vec3 {
9
13
  ) {
10
14
  }
11
15
 
12
- // Returns the x, y components of this
16
+ /** Returns the x, y components of this. */
13
17
  public get xy(): { x: number; y: number } {
14
18
  // Useful for APIs that behave differently if .z is present.
15
19
  return {
@@ -22,7 +26,7 @@ export default class Vec3 {
22
26
  return new Vec3(x, y, z);
23
27
  }
24
28
 
25
- // Returns this' [idx]th component
29
+ /** Returns this' `idx`th component. For example, `Vec3.of(1, 2, 3).at(1) → 2`. */
26
30
  public at(idx: number): number {
27
31
  if (idx === 0) return this.x;
28
32
  if (idx === 1) return this.y;
@@ -31,7 +35,7 @@ export default class Vec3 {
31
35
  throw new Error(`${idx} out of bounds!`);
32
36
  }
33
37
 
34
- // Alias for this.magnitude
38
+ /** Alias for this.magnitude. */
35
39
  public length(): number {
36
40
  return this.magnitude();
37
41
  }
@@ -44,16 +48,26 @@ export default class Vec3 {
44
48
  return this.dot(this);
45
49
  }
46
50
 
47
- // Return this' angle in the XY plane (treats this as a Vec2)
51
+ /**
52
+ * Return this' angle in the XY plane (treats this as a Vec2).
53
+ *
54
+ * This is equivalent to `Math.atan2(vec.y, vec.x)`.
55
+ */
48
56
  public angle(): number {
49
57
  return Math.atan2(this.y, this.x);
50
58
  }
51
59
 
60
+ /**
61
+ * Returns a unit vector in the same direction as this.
62
+ *
63
+ * If `this` has zero length, the resultant vector has `NaN` components.
64
+ */
52
65
  public normalized(): Vec3 {
53
66
  const norm = this.magnitude();
54
67
  return Vec3.of(this.x / norm, this.y / norm, this.z / norm);
55
68
  }
56
69
 
70
+ /** @returns A copy of `this` multiplied by a scalar. */
57
71
  public times(c: number): Vec3 {
58
72
  return Vec3.of(this.x * c, this.y * c, this.z * c);
59
73
  }
@@ -81,8 +95,10 @@ export default class Vec3 {
81
95
  );
82
96
  }
83
97
 
84
- // Returns a vector orthogonal to this. If this is a Vec2, returns [this] rotated
85
- // 90 degrees counter-clockwise.
98
+ /**
99
+ * Returns a vector orthogonal to this. If this is a Vec2, returns `this` rotated
100
+ * 90 degrees counter-clockwise.
101
+ */
86
102
  public orthog(): Vec3 {
87
103
  // If parallel to the z-axis
88
104
  if (this.dot(Vec3.unitX) === 0 && this.dot(Vec3.unitY) === 0) {
@@ -92,18 +108,32 @@ export default class Vec3 {
92
108
  return this.cross(Vec3.unitZ.times(-1)).normalized();
93
109
  }
94
110
 
95
- // Returns this plus a vector of length [distance] in [direction]
111
+ /** Returns this plus a vector of length `distance` in `direction`. */
96
112
  public extend(distance: number, direction: Vec3): Vec3 {
97
113
  return this.plus(direction.normalized().times(distance));
98
114
  }
99
115
 
100
- // Returns a vector [fractionTo] of the way to target from this.
116
+ /** Returns a vector `fractionTo` of the way to target from this. */
101
117
  public lerp(target: Vec3, fractionTo: number): Vec3 {
102
118
  return this.times(1 - fractionTo).plus(target.times(fractionTo));
103
119
  }
104
120
 
105
- // [zip] Maps a component of this and a corresponding component of
106
- // [other] to a component of the output vector.
121
+ /**
122
+ * `zip` Maps a component of this and a corresponding component of
123
+ * `other` to a component of the output vector.
124
+ *
125
+ * @example
126
+ * ```
127
+ * const a = Vec3.of(1, 2, 3);
128
+ * const b = Vec3.of(0.5, 2.1, 2.9);
129
+ *
130
+ * const zipped = a.zip(b, (aComponent, bComponent) => {
131
+ * return Math.min(aComponent, bComponent);
132
+ * });
133
+ *
134
+ * console.log(zipped.toString()); // → Vec(0.5, 2, 2.9)
135
+ * ```
136
+ */
107
137
  public zip(
108
138
  other: Vec3, zip: (componentInThis: number, componentInOther: number)=> number
109
139
  ): Vec3 {
@@ -114,10 +144,17 @@ export default class Vec3 {
114
144
  );
115
145
  }
116
146
 
117
- // Returns a vector with each component acted on by [fn]
118
- public map(fn: (component: number)=> number): Vec3 {
147
+ /**
148
+ * Returns a vector with each component acted on by `fn`.
149
+ *
150
+ * @example
151
+ * ```
152
+ * console.log(Vec3.of(1, 2, 3).map(val => val + 1)); // → Vec(2, 3, 4)
153
+ * ```
154
+ */
155
+ public map(fn: (component: number, index: number)=> number): Vec3 {
119
156
  return Vec3.of(
120
- fn(this.x), fn(this.y), fn(this.z)
157
+ fn(this.x, 0), fn(this.y, 1), fn(this.z, 2)
121
158
  );
122
159
  }
123
160
 
@@ -125,8 +162,19 @@ export default class Vec3 {
125
162
  return [this.x, this.y, this.z];
126
163
  }
127
164
 
128
- // [fuzz] The maximum difference between two components for this and [other]
129
- // to be considered equal.
165
+ /**
166
+ * [fuzz] The maximum difference between two components for this and [other]
167
+ * to be considered equal.
168
+ *
169
+ * @example
170
+ * ```
171
+ * Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 100); // → true
172
+ * Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 0.1); // → false
173
+ * Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 3); // → true
174
+ * Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 3.01); // → true
175
+ * Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 2.99); // → false
176
+ * ```
177
+ */
130
178
  public eq(other: Vec3, fuzz: number): boolean {
131
179
  for (let i = 0; i < 3; i++) {
132
180
  if (Math.abs(other.at(i) - this.at(i)) > fuzz) {
@@ -0,0 +1,15 @@
1
+ import LineSegment2 from './LineSegment2';
2
+ import Mat33 from './Mat33';
3
+ import Path from './Path';
4
+ import Rect2 from './Rect2';
5
+ import { Vec2 } from './Vec2';
6
+ import Vec3 from './Vec3';
7
+
8
+ export {
9
+ LineSegment2,
10
+ Mat33,
11
+ Path,
12
+ Rect2,
13
+ Vec3,
14
+ Vec2,
15
+ };
@@ -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
+ });