js-draw 0.1.0 → 0.1.3

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 (102) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +2 -2
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.js +6 -3
  5. package/dist/src/EditorImage.d.ts +1 -1
  6. package/dist/src/EditorImage.js +6 -3
  7. package/dist/src/Pointer.d.ts +3 -2
  8. package/dist/src/Pointer.js +12 -3
  9. package/dist/src/SVGLoader.d.ts +11 -0
  10. package/dist/src/SVGLoader.js +113 -4
  11. package/dist/src/Viewport.d.ts +1 -1
  12. package/dist/src/Viewport.js +12 -2
  13. package/dist/src/components/AbstractComponent.d.ts +6 -0
  14. package/dist/src/components/AbstractComponent.js +11 -0
  15. package/dist/src/components/SVGGlobalAttributesObject.js +0 -1
  16. package/dist/src/components/Stroke.js +1 -1
  17. package/dist/src/components/Text.d.ts +30 -0
  18. package/dist/src/components/Text.js +109 -0
  19. package/dist/src/components/builders/FreehandLineBuilder.js +1 -1
  20. package/dist/src/components/localization.d.ts +1 -0
  21. package/dist/src/components/localization.js +1 -0
  22. package/dist/src/geometry/Mat33.d.ts +1 -0
  23. package/dist/src/geometry/Mat33.js +30 -0
  24. package/dist/src/geometry/Path.js +105 -67
  25. package/dist/src/geometry/Rect2.d.ts +2 -0
  26. package/dist/src/geometry/Rect2.js +25 -8
  27. package/dist/src/rendering/Display.js +4 -3
  28. package/dist/src/rendering/caching/CacheRecord.js +2 -1
  29. package/dist/src/rendering/caching/CacheRecordManager.js +2 -10
  30. package/dist/src/rendering/caching/RenderingCache.js +10 -4
  31. package/dist/src/rendering/caching/RenderingCacheNode.js +10 -3
  32. package/dist/src/rendering/caching/testUtils.js +1 -1
  33. package/dist/src/rendering/caching/types.d.ts +1 -0
  34. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -1
  35. package/dist/src/rendering/renderers/AbstractRenderer.js +13 -1
  36. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +3 -0
  37. package/dist/src/rendering/renderers/CanvasRenderer.js +28 -8
  38. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -0
  39. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  40. package/dist/src/rendering/renderers/SVGRenderer.d.ts +6 -2
  41. package/dist/src/rendering/renderers/SVGRenderer.js +50 -7
  42. package/dist/src/testing/loadExpectExtensions.js +1 -4
  43. package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
  44. package/dist/src/toolbar/HTMLToolbar.js +216 -154
  45. package/dist/src/toolbar/icons.d.ts +12 -0
  46. package/dist/src/toolbar/icons.js +197 -0
  47. package/dist/src/toolbar/localization.d.ts +4 -1
  48. package/dist/src/toolbar/localization.js +4 -1
  49. package/dist/src/toolbar/types.d.ts +4 -0
  50. package/dist/src/tools/PanZoom.d.ts +9 -6
  51. package/dist/src/tools/PanZoom.js +30 -21
  52. package/dist/src/tools/Pen.js +8 -3
  53. package/dist/src/tools/SelectionTool.js +1 -1
  54. package/dist/src/tools/TextTool.d.ts +29 -0
  55. package/dist/src/tools/TextTool.js +154 -0
  56. package/dist/src/tools/ToolController.d.ts +5 -5
  57. package/dist/src/tools/ToolController.js +10 -9
  58. package/dist/src/tools/localization.d.ts +3 -0
  59. package/dist/src/tools/localization.js +3 -0
  60. package/package.json +1 -1
  61. package/src/Editor.ts +7 -3
  62. package/src/EditorImage.ts +7 -3
  63. package/src/Pointer.ts +13 -4
  64. package/src/SVGLoader.ts +146 -5
  65. package/src/Viewport.ts +15 -3
  66. package/src/components/AbstractComponent.ts +16 -1
  67. package/src/components/SVGGlobalAttributesObject.ts +0 -1
  68. package/src/components/Stroke.ts +1 -1
  69. package/src/components/Text.ts +136 -0
  70. package/src/components/builders/FreehandLineBuilder.ts +1 -1
  71. package/src/components/localization.ts +2 -0
  72. package/src/geometry/Mat33.test.ts +44 -0
  73. package/src/geometry/Mat33.ts +41 -0
  74. package/src/geometry/Path.fromString.test.ts +94 -4
  75. package/src/geometry/Path.toString.test.ts +7 -3
  76. package/src/geometry/Path.ts +110 -68
  77. package/src/geometry/Rect2.test.ts +9 -0
  78. package/src/geometry/Rect2.ts +33 -8
  79. package/src/rendering/Display.ts +4 -3
  80. package/src/rendering/caching/CacheRecord.ts +2 -1
  81. package/src/rendering/caching/CacheRecordManager.ts +2 -12
  82. package/src/rendering/caching/RenderingCache.test.ts +1 -1
  83. package/src/rendering/caching/RenderingCache.ts +11 -4
  84. package/src/rendering/caching/RenderingCacheNode.ts +16 -3
  85. package/src/rendering/caching/testUtils.ts +1 -0
  86. package/src/rendering/caching/types.ts +4 -0
  87. package/src/rendering/renderers/AbstractRenderer.ts +18 -1
  88. package/src/rendering/renderers/CanvasRenderer.ts +34 -10
  89. package/src/rendering/renderers/DummyRenderer.ts +8 -0
  90. package/src/rendering/renderers/SVGRenderer.ts +57 -10
  91. package/src/testing/loadExpectExtensions.ts +1 -4
  92. package/src/toolbar/HTMLToolbar.ts +262 -170
  93. package/src/toolbar/icons.ts +226 -0
  94. package/src/toolbar/localization.ts +9 -2
  95. package/src/toolbar/toolbar.css +21 -8
  96. package/src/toolbar/types.ts +5 -0
  97. package/src/tools/PanZoom.ts +37 -27
  98. package/src/tools/Pen.ts +7 -3
  99. package/src/tools/SelectionTool.ts +1 -1
  100. package/src/tools/TextTool.ts +206 -0
  101. package/src/tools/ToolController.ts +7 -5
  102. package/src/tools/localization.ts +7 -0
@@ -141,4 +141,48 @@ describe('Mat33 tests', () => {
141
141
  fullTransformInverse.transformVec2(fullTransform.transformVec2(Vec2.unitX))
142
142
  ).objEq(Vec2.unitX, fuzz);
143
143
  });
144
+
145
+ it('should convert CSS matrix(...) strings to matricies', () => {
146
+ // From MDN:
147
+ // ⎡ a c e ⎤
148
+ // ⎢ b d f ⎥ = matrix(a,b,c,d,e,f)
149
+ // ⎣ 0 0 1 ⎦
150
+ const identity = Mat33.fromCSSMatrix('matrix(1, 0, 0, 1, 0, 0)');
151
+ expect(identity).objEq(Mat33.identity);
152
+ expect(Mat33.fromCSSMatrix('matrix(1, 2, 3, 4, 5, 6)')).objEq(new Mat33(
153
+ 1, 3, 5,
154
+ 2, 4, 6,
155
+ 0, 0, 1,
156
+ ));
157
+ expect(Mat33.fromCSSMatrix('matrix(1e2, 2, 3, 4, 5, 6)')).objEq(new Mat33(
158
+ 1e2, 3, 5,
159
+ 2, 4, 6,
160
+ 0, 0, 1,
161
+ ));
162
+ expect(Mat33.fromCSSMatrix('matrix(1.6, 2, .3, 4, 5, 6)')).objEq(new Mat33(
163
+ 1.6, .3, 5,
164
+ 2, 4, 6,
165
+ 0, 0, 1,
166
+ ));
167
+ expect(Mat33.fromCSSMatrix('matrix(-1, 2, 3.E-2, 4, -5.123, -6.5)')).objEq(new Mat33(
168
+ -1, 0.03, -5.123,
169
+ 2, 4, -6.5,
170
+ 0, 0, 1,
171
+ ));
172
+ expect(Mat33.fromCSSMatrix('matrix(1.6,\n\t2, .3, 4, 5, 6)')).objEq(new Mat33(
173
+ 1.6, .3, 5,
174
+ 2, 4, 6,
175
+ 0, 0, 1,
176
+ ));
177
+ expect(Mat33.fromCSSMatrix('matrix(1.6,2, .3E-2, 4, 5, 6)')).objEq(new Mat33(
178
+ 1.6, 3e-3, 5,
179
+ 2, 4, 6,
180
+ 0, 0, 1,
181
+ ));
182
+ expect(Mat33.fromCSSMatrix('matrix(-1, 2e6, 3E-2,-5.123, -6.5e-1, 0.01)')).objEq(new Mat33(
183
+ -1, 3E-2, -6.5e-1,
184
+ 2e6, -5.123, 0.01,
185
+ 0, 0, 1,
186
+ ));
187
+ });
144
188
  });
@@ -268,4 +268,45 @@ export default class Mat33 {
268
268
  // Translate such that [center] goes to (0, 0)
269
269
  return result.rightMul(Mat33.translation(center.times(-1)));
270
270
  }
271
+
272
+ // Converts a CSS-form matrix(a, b, c, d, e, f) to a Mat33.
273
+ public static fromCSSMatrix(cssString: string): Mat33 {
274
+ if (cssString === '' || cssString === 'none') {
275
+ return Mat33.identity;
276
+ }
277
+
278
+ const numberExp = '([-]?\\d*(?:\\.\\d*)?(?:[eE][-]?\\d+)?)';
279
+ const numberSepExp = '[, \\t\\n]';
280
+ const regExpSource = `^\\s*matrix\\s*\\(${
281
+ [
282
+ // According to MDN, matrix(a,b,c,d,e,f) has form:
283
+ // ⎡ a c e ⎤
284
+ // ⎢ b d f ⎥
285
+ // ⎣ 0 0 1 ⎦
286
+ numberExp, numberExp, numberExp, // a, c, e
287
+ numberExp, numberExp, numberExp, // b, d, f
288
+ ].join(`${numberSepExp}+`)
289
+ }${numberSepExp}*\\)\\s*$`;
290
+ const matrixExp = new RegExp(regExpSource, 'i');
291
+ const match = matrixExp.exec(cssString);
292
+
293
+ if (!match) {
294
+ throw new Error(`Unsupported transformation: ${cssString}`);
295
+ }
296
+
297
+ const matrixData = match.slice(1).map(entry => parseFloat(entry));
298
+ const a = matrixData[0];
299
+ const b = matrixData[1];
300
+ const c = matrixData[2];
301
+ const d = matrixData[3];
302
+ const e = matrixData[4];
303
+ const f = matrixData[5];
304
+
305
+ const transform = new Mat33(
306
+ a, c, e,
307
+ b, d, f,
308
+ 0, 0, 1
309
+ );
310
+ return transform;
311
+ }
271
312
  }
@@ -90,15 +90,38 @@ describe('Path.fromString', () => {
90
90
  ]);
91
91
  });
92
92
 
93
+ it('should break compoents at -s', () => {
94
+ const path = Path.fromString('m1-1 L-1-1-3-4-5-6,5-1');
95
+ expect(path.parts.length).toBe(4);
96
+ expect(path.parts).toMatchObject([
97
+ {
98
+ kind: PathCommandType.LineTo,
99
+ point: Vec2.of(-1, -1),
100
+ },
101
+ {
102
+ kind: PathCommandType.LineTo,
103
+ point: Vec2.of(-3, -4),
104
+ },
105
+ {
106
+ kind: PathCommandType.LineTo,
107
+ point: Vec2.of(-5, -6),
108
+ },
109
+ {
110
+ kind: PathCommandType.LineTo,
111
+ point: Vec2.of(5, -1),
112
+ },
113
+ ]);
114
+ });
115
+
93
116
  it('should properly handle cubic Bézier curves', () => {
94
- const path = Path.fromString('c1,1 0,-3 4 5 C1,1 0.1, 0.1 0, 0');
117
+ const path = Path.fromString('m1,1 c1,1 0-3 4 5 C1,1 0.1, 0.1 0, 0');
95
118
  expect(path.parts.length).toBe(2);
96
119
  expect(path.parts).toMatchObject([
97
120
  {
98
121
  kind: PathCommandType.CubicBezierTo,
99
- controlPoint1: Vec2.of(1, 1),
122
+ controlPoint1: Vec2.of(2, 2),
100
123
  controlPoint2: Vec2.of(1, -2),
101
- endPoint: Vec2.of(5, 3),
124
+ endPoint: Vec2.of(5, 6),
102
125
  },
103
126
  {
104
127
  kind: PathCommandType.CubicBezierTo,
@@ -120,7 +143,7 @@ describe('Path.fromString', () => {
120
143
  {
121
144
  kind: PathCommandType.QuadraticBezierTo,
122
145
  controlPoint: Vec2.of(1, 1),
123
- endPoint: Vec2.of(-2, -3),
146
+ endPoint: Vec2.of(-1, -1),
124
147
  },
125
148
  {
126
149
  kind: PathCommandType.QuadraticBezierTo,
@@ -130,4 +153,71 @@ describe('Path.fromString', () => {
130
153
  ]);
131
154
  expect(path.startPoint).toMatchObject(Vec2.of(1, 1));
132
155
  });
156
+
157
+ it('should correctly handle a command followed by multiple sets of arguments', () => {
158
+ // Commands followed by multiple sets of arguments, for example,
159
+ // l 5,10 5,4 3,2,
160
+ // should be interpreted as multiple commands. Our example, is therefore equivalent to,
161
+ // l 5,10 l 5,4 l 3,2
162
+
163
+ const path = Path.fromString(`
164
+ L5,10 1,1
165
+ 2,2 -3,-1
166
+ q 1,2 1,1
167
+ -1,-1 -3,-4
168
+ h -4 -1
169
+ V 3 5 1
170
+ `);
171
+ expect(path.parts).toMatchObject([
172
+ {
173
+ kind: PathCommandType.LineTo,
174
+ point: Vec2.of(1, 1),
175
+ },
176
+ {
177
+ kind: PathCommandType.LineTo,
178
+ point: Vec2.of(2, 2),
179
+ },
180
+ {
181
+ kind: PathCommandType.LineTo,
182
+ point: Vec2.of(-3, -1),
183
+ },
184
+
185
+ // q 1,2 1,1 -1,-1 -3,-4
186
+ {
187
+ kind: PathCommandType.QuadraticBezierTo,
188
+ controlPoint: Vec2.of(-2, 1),
189
+ endPoint: Vec2.of(-2, 0),
190
+ },
191
+ {
192
+ kind: PathCommandType.QuadraticBezierTo,
193
+ controlPoint: Vec2.of(-3, -1),
194
+ endPoint: Vec2.of(-5, -4),
195
+ },
196
+
197
+ // h -4 -1
198
+ {
199
+ kind: PathCommandType.LineTo,
200
+ point: Vec2.of(-9, -4),
201
+ },
202
+ {
203
+ kind: PathCommandType.LineTo,
204
+ point: Vec2.of(-10, -4),
205
+ },
206
+
207
+ // V 3 5 1
208
+ {
209
+ kind: PathCommandType.LineTo,
210
+ point: Vec2.of(-10, 3),
211
+ },
212
+ {
213
+ kind: PathCommandType.LineTo,
214
+ point: Vec2.of(-10, 5),
215
+ },
216
+ {
217
+ kind: PathCommandType.LineTo,
218
+ point: Vec2.of(-10, 1),
219
+ },
220
+ ]);
221
+ expect(path.startPoint).toMatchObject(Vec2.of(5, 10));
222
+ });
133
223
  });
@@ -19,14 +19,18 @@ describe('Path.toString', () => {
19
19
  });
20
20
 
21
21
  it('should fix rounding errors', () => {
22
- const path = new Path(Vec2.of(0.10000001, 0.19999999), [
22
+ const path = new Path(Vec2.of(0.100000001, 0.199999999), [
23
23
  {
24
24
  kind: PathCommandType.QuadraticBezierTo,
25
25
  controlPoint: Vec2.of(9999, -10.999999995),
26
- endPoint: Vec2.of(0.000300001, 1.40000002),
26
+ endPoint: Vec2.of(0.000300001, 1.400000002),
27
27
  },
28
+ {
29
+ kind: PathCommandType.LineTo,
30
+ point: Vec2.of(184.00482359999998, 1)
31
+ }
28
32
  ]);
29
- expect(path.toString()).toBe('M0.1,0.2Q9999,-11 0.0003,1.4');
33
+ expect(path.toString()).toBe('M0.1,0.2Q9999,-11 0.0003,1.4L184.0048236,1');
30
34
  });
31
35
 
32
36
  it('should not remove trailing zeroes before decimal points', () => {
@@ -303,6 +303,8 @@ export default class Path {
303
303
  const postDecimal = parseInt(roundingDownMatch[3], 10);
304
304
  const preDecimal = parseInt(roundingDownMatch[2], 10);
305
305
 
306
+ const origPostDecimalString = roundingDownMatch[3];
307
+
306
308
  let newPostDecimal = (postDecimal + 10 - lastDigit).toString();
307
309
  let carry = 0;
308
310
  if (newPostDecimal.length > postDecimal.toString().length) {
@@ -310,13 +312,21 @@ export default class Path {
310
312
  newPostDecimal = newPostDecimal.substring(1);
311
313
  carry = 1;
312
314
  }
315
+
316
+ // parseInt(...).toString() removes leading zeroes. Add them back.
317
+ while (newPostDecimal.length < origPostDecimalString.length) {
318
+ newPostDecimal = carry.toString(10) + newPostDecimal;
319
+ carry = 0;
320
+ }
321
+
313
322
  text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
314
323
  }
315
324
 
316
325
  text = text.replace(fixRoundingUpExp, '$1');
317
326
 
318
327
  // Remove trailing zeroes
319
- text = text.replace(/([.][^0]*)0+$/, '$1');
328
+ text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
329
+ text = text.replace(/[.]0+$/, '.');
320
330
 
321
331
  // Remove trailing period
322
332
  return text.replace(/[.]$/, '');
@@ -371,6 +381,7 @@ export default class Path {
371
381
 
372
382
  let lastPos: Point2 = Vec2.zero;
373
383
  let firstPos: Point2|null = null;
384
+ let startPos: Point2|null = null;
374
385
  let isFirstCommand: boolean = true;
375
386
  const commands: PathCommand[] = [];
376
387
 
@@ -413,19 +424,67 @@ export default class Path {
413
424
  endPoint,
414
425
  });
415
426
  };
427
+ const commandArgCounts: Record<string, number> = {
428
+ 'm': 1,
429
+ 'l': 1,
430
+ 'c': 3,
431
+ 'q': 2,
432
+ 'z': 0,
433
+ 'h': 1,
434
+ 'v': 1,
435
+ };
416
436
 
417
437
  // Each command: Command character followed by anything that isn't a command character
418
- const commandExp = /([MmZzLlHhVvCcSsQqTtAa])\s*([^a-zA-Z]*)/g;
438
+ const commandExp = /([MZLHVCSQTA])\s*([^MZLHVCSQTA]*)/ig;
419
439
  let current;
420
440
  while ((current = commandExp.exec(pathString)) !== null) {
421
- const argParts = current[2].trim().split(/[^0-9.-]/).filter(
441
+ const argParts = current[2].trim().split(/[^0-9Ee.-]/).filter(
422
442
  part => part.length > 0
423
- );
424
- const numericArgs = argParts.map(arg => parseFloat(arg));
443
+ ).reduce((accumualtor: string[], current: string): string[] => {
444
+ // As of 09/2022, iOS Safari doesn't support support lookbehind in regular
445
+ // expressions. As such, we need an alternative.
446
+ // Because '-' can be used as a path separator, unless preceeded by an 'e' (as in 1e-5),
447
+ // we need special cases:
448
+ current = current.replace(/([^eE])[-]/g, '$1 -');
449
+ const parts = current.split(' -');
450
+ if (parts[0] !== '') {
451
+ accumualtor.push(parts[0]);
452
+ }
453
+ accumualtor.push(...parts.slice(1).map(part => `-${part}`));
454
+ return accumualtor;
455
+ }, []);
456
+
457
+ let numericArgs = argParts.map(arg => parseFloat(arg));
458
+
459
+ let commandChar = current[1].toLowerCase();
460
+ let uppercaseCommand = current[1] !== commandChar;
461
+
462
+ // Convert commands that don't take points into commands that do.
463
+ if (commandChar === 'v' || commandChar === 'h') {
464
+ numericArgs = numericArgs.reduce((accumulator: number[], current: number): number[] => {
465
+ if (commandChar === 'v') {
466
+ return accumulator.concat(uppercaseCommand ? lastPos.x : 0, current);
467
+ } else {
468
+ return accumulator.concat(current, uppercaseCommand ? lastPos.y : 0);
469
+ }
470
+ }, []);
471
+ commandChar = 'l';
472
+ } else if (commandChar === 'z') {
473
+ if (firstPos) {
474
+ numericArgs = [ firstPos.x, firstPos.y ];
475
+ firstPos = lastPos;
476
+ } else {
477
+ continue;
478
+ }
479
+
480
+ // 'z' always acts like an uppercase lineTo(startPos)
481
+ uppercaseCommand = true;
482
+ commandChar = 'l';
483
+ }
484
+
425
485
 
426
- const commandChar = current[1];
427
- const uppercaseCommand = commandChar !== commandChar.toLowerCase();
428
- const args = numericArgs.reduce((
486
+ const commandArgCount: number = commandArgCounts[commandChar] ?? 0;
487
+ const allArgs = numericArgs.reduce((
429
488
  accumulator: Point2[], current, index, parts
430
489
  ): Point2[] => {
431
490
  if (index % 2 !== 0) {
@@ -435,82 +494,65 @@ export default class Path {
435
494
  } else {
436
495
  return accumulator;
437
496
  }
438
- }, []).map((coordinate: Vec2): Point2 => {
497
+ }, []).map((coordinate, index): Point2 => {
439
498
  // Lowercase commands are relative, uppercase commands use absolute
440
499
  // positioning
500
+ let newPos;
441
501
  if (uppercaseCommand) {
442
- lastPos = coordinate;
443
- return coordinate;
502
+ newPos = coordinate;
444
503
  } else {
445
- lastPos = lastPos.plus(coordinate);
446
- return lastPos;
504
+ newPos = lastPos.plus(coordinate);
447
505
  }
448
- });
449
-
450
- let expectedPointArgCount;
451
506
 
452
- switch (commandChar.toLowerCase()) {
453
- case 'm':
454
- expectedPointArgCount = 1;
455
- moveTo(args[0]);
456
- break;
457
- case 'l':
458
- expectedPointArgCount = 1;
459
- lineTo(args[0]);
460
- break;
461
- case 'z':
462
- expectedPointArgCount = 0;
463
- // firstPos can be null if the stroke data is just 'z'.
464
- if (firstPos) {
465
- lineTo(firstPos);
507
+ if ((index + 1) % commandArgCount === 0) {
508
+ lastPos = newPos;
466
509
  }
467
- break;
468
- case 'c':
469
- expectedPointArgCount = 3;
470
- cubicBezierTo(args[0], args[1], args[2]);
471
- break;
472
- case 'q':
473
- expectedPointArgCount = 2;
474
- quadraticBeierTo(args[0], args[1]);
475
- break;
476
-
477
- // Horizontal line
478
- case 'h':
479
- expectedPointArgCount = 0;
480
510
 
481
- if (uppercaseCommand) {
482
- lineTo(Vec2.of(numericArgs[0], lastPos.y));
483
- } else {
484
- lineTo(lastPos.plus(Vec2.of(numericArgs[0], 0)));
485
- }
486
- break;
511
+ return newPos;
512
+ });
513
+
514
+ if (allArgs.length % commandArgCount !== 0) {
515
+ throw new Error([
516
+ `Incorrect number of arguments: got ${JSON.stringify(allArgs)} with a length of ${allArgs.length} ≠ ${commandArgCount}k, k ∈ ℤ.`,
517
+ `The number of arguments to ${commandChar} must be a multiple of ${commandArgCount}!`,
518
+ `Command: ${current[0]}`,
519
+ ].join('\n'));
520
+ }
487
521
 
488
- // Vertical line
489
- case 'v':
490
- expectedPointArgCount = 0;
522
+ for (let argPos = 0; argPos < allArgs.length; argPos += commandArgCount) {
523
+ const args = allArgs.slice(argPos, argPos + commandArgCount);
491
524
 
492
- if (uppercaseCommand) {
493
- lineTo(Vec2.of(lastPos.x, numericArgs[1]));
494
- } else {
495
- lineTo(lastPos.plus(Vec2.of(0, numericArgs[1])));
525
+ switch (commandChar.toLowerCase()) {
526
+ case 'm':
527
+ if (argPos === 0) {
528
+ moveTo(args[0]);
529
+ } else {
530
+ lineTo(args[0]);
531
+ }
532
+ break;
533
+ case 'l':
534
+ lineTo(args[0]);
535
+ break;
536
+ case 'c':
537
+ cubicBezierTo(args[0], args[1], args[2]);
538
+ break;
539
+ case 'q':
540
+ quadraticBeierTo(args[0], args[1]);
541
+ break;
542
+ default:
543
+ throw new Error(`Unknown path command ${commandChar}`);
496
544
  }
497
- break;
498
- default:
499
- throw new Error(`Unknown path command ${commandChar}`);
500
- }
501
545
 
502
- if (args.length !== expectedPointArgCount) {
503
- throw new Error(`
504
- Incorrect number of arguments: got ${JSON.stringify(args)} with a length of ${args.length} ≠ ${expectedPointArgCount}.
505
- `.trim());
546
+ isFirstCommand = false;
506
547
  }
507
548
 
508
- if (args.length > 0) {
509
- firstPos ??= args[0];
549
+ if (allArgs.length > 0) {
550
+ firstPos ??= allArgs[0];
551
+ startPos ??= firstPos;
552
+ lastPos = allArgs[allArgs.length - 1];
510
553
  }
511
- isFirstCommand = false;
512
554
  }
513
555
 
514
- return new Path(firstPos ?? Vec2.zero, commands);
556
+ return new Path(startPos ?? Vec2.zero, commands);
515
557
  }
516
558
  }
@@ -148,4 +148,13 @@ describe('Rect2', () => {
148
148
  expect(Rect2.empty.divideIntoGrid(1000, 10000).length).toBe(1);
149
149
  });
150
150
  });
151
+
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
+ );
159
+ });
151
160
  });
@@ -67,22 +67,39 @@ export default class Rect2 {
67
67
  }
68
68
 
69
69
  public intersects(other: Rect2): boolean {
70
- return this.intersection(other) !== null;
70
+ // Project along x/y axes.
71
+ const thisMinX = this.x;
72
+ const thisMaxX = thisMinX + this.w;
73
+ const otherMinX = other.x;
74
+ const otherMaxX = other.x + other.w;
75
+
76
+ if (thisMaxX < otherMinX || thisMinX > otherMaxX) {
77
+ return false;
78
+ }
79
+
80
+
81
+ const thisMinY = this.y;
82
+ const thisMaxY = thisMinY + this.h;
83
+ const otherMinY = other.y;
84
+ const otherMaxY = other.y + other.h;
85
+
86
+ if (thisMaxY < otherMinY || thisMinY > otherMaxY) {
87
+ return false;
88
+ }
89
+
90
+ return true;
71
91
  }
72
92
 
73
93
  // Returns the overlap of this and [other], or null, if no such
74
94
  // overlap exists
75
95
  public intersection(other: Rect2): Rect2|null {
76
- const topLeft = this.topLeft.zip(other.topLeft, Math.max);
77
- const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
78
-
79
- // The intersection can't be outside of this rectangle
80
- if (!this.containsPoint(topLeft) || !this.containsPoint(bottomRight)) {
81
- return null;
82
- } else if (!other.containsPoint(topLeft) || !other.containsPoint(bottomRight)) {
96
+ if (!this.intersects(other)) {
83
97
  return null;
84
98
  }
85
99
 
100
+ const topLeft = this.topLeft.zip(other.topLeft, Math.max);
101
+ const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
102
+
86
103
  return Rect2.fromCorners(topLeft, bottomRight);
87
104
  }
88
105
 
@@ -167,6 +184,14 @@ export default class Rect2 {
167
184
  return this.topLeft.plus(Vec2.of(0, this.h));
168
185
  }
169
186
 
187
+ public get width() {
188
+ return this.w;
189
+ }
190
+
191
+ public get height() {
192
+ return this.h;
193
+ }
194
+
170
195
  // Returns edges in the order
171
196
  // [ rightEdge, topEdge, leftEdge, bottomEdge ]
172
197
  public getEdges(): LineSegment2[] {
@@ -31,7 +31,7 @@ export default class Display {
31
31
  throw new Error(`Unknown rendering mode, ${mode}!`);
32
32
  }
33
33
 
34
- const cacheBlockResolution = Vec2.of(500, 500);
34
+ const cacheBlockResolution = Vec2.of(600, 600);
35
35
  this.cache = new RenderingCache({
36
36
  createRenderer: () => {
37
37
  if (mode === RenderingMode.DummyRenderer) {
@@ -54,8 +54,9 @@ export default class Display {
54
54
  },
55
55
  blockResolution: cacheBlockResolution,
56
56
  cacheSize: 500 * 500 * 4 * 200,
57
- maxScale: 1.4,
58
- minComponentsPerCache: 10,
57
+ maxScale: 1.5,
58
+ minComponentsPerCache: 50,
59
+ minComponentsToUseCache: 120,
59
60
  });
60
61
 
61
62
  this.editor.notifier.on(EditorEventType.DisplayResized, event => {
@@ -21,7 +21,7 @@ export default class CacheRecord {
21
21
  }
22
22
 
23
23
  public startRender(): AbstractRenderer {
24
- this.lastUsedCycle = this.cacheState.currentRenderingCycle++;
24
+ this.lastUsedCycle = this.cacheState.currentRenderingCycle;
25
25
  if (!this.allocd) {
26
26
  throw new Error('Only alloc\'d canvases can be rendered to');
27
27
  }
@@ -45,6 +45,7 @@ export default class CacheRecord {
45
45
  }
46
46
  this.allocd = true;
47
47
  this.onBeforeDeallocCallback = newDeallocCallback;
48
+ this.lastUsedCycle = this.cacheState.currentRenderingCycle;
48
49
  }
49
50
 
50
51
  public getLastUsedCycle(): number {
@@ -39,17 +39,7 @@ export class CacheRecordManager {
39
39
 
40
40
  // Returns null if there are no cache records. Returns an unalloc'd record if one exists.
41
41
  private getLeastRecentlyUsedRecord(): CacheRecord|null {
42
- let lruSoFar: CacheRecord|null = null;
43
- for (const rec of this.cacheRecords) {
44
- if (!rec.isAllocd()) {
45
- return rec;
46
- }
47
-
48
- if (!lruSoFar || rec.getLastUsedCycle() < lruSoFar.getLastUsedCycle()) {
49
- lruSoFar = rec;
50
- }
51
- }
52
-
53
- return lruSoFar;
42
+ this.cacheRecords.sort((a, b) => a.getLastUsedCycle() - b.getLastUsedCycle());
43
+ return this.cacheRecords[0];
54
44
  }
55
45
  }
@@ -10,7 +10,7 @@ import Viewport from '../../Viewport';
10
10
  import Mat33 from '../../geometry/Mat33';
11
11
 
12
12
  describe('RenderingCache', () => {
13
- const testPath = Path.fromString('M0,0 l100,500 l-20,20');
13
+ const testPath = Path.fromString('M0,0 l100,500 l-20,20 L-100,-100');
14
14
  const testStroke = new Stroke([ testPath.toRenderable({ fill: Color4.purple }) ]);
15
15
 
16
16
  it('should create a root node large enough to contain the viewport', () => {
@@ -37,11 +37,12 @@ export default class RenderingCache {
37
37
  }
38
38
 
39
39
  if (!this.rootNode) {
40
- // Ensure that the node is just big enough to contain the entire viewport.
41
- const rootNodeSize = visibleRect.maxDimension;
40
+ // Adjust the node so that it has the correct aspect ratio
41
+ const res = this.partialSharedState.props.blockResolution;
42
+
42
43
  const topLeft = visibleRect.topLeft;
43
44
  this.rootNode = new RenderingCacheNode(
44
- new Rect2(topLeft.x, topLeft.y, rootNodeSize, rootNodeSize),
45
+ new Rect2(topLeft.x, topLeft.y, res.x, res.y),
45
46
  this.getSharedState()
46
47
  );
47
48
  }
@@ -51,6 +52,12 @@ export default class RenderingCache {
51
52
  }
52
53
 
53
54
  this.rootNode = this.rootNode!.smallestChildContaining(visibleRect) ?? this.rootNode;
54
- this.rootNode!.renderItems(screenRenderer, [ image ], viewport);
55
+
56
+ const visibleLeaves = image.getLeavesIntersectingRegion(viewport.visibleRect, rect => screenRenderer.isTooSmallToRender(rect));
57
+ if (visibleLeaves.length > this.partialSharedState.props.minComponentsToUseCache) {
58
+ this.rootNode!.renderItems(screenRenderer, [ image ], viewport);
59
+ } else {
60
+ image.render(screenRenderer, visibleRect);
61
+ }
55
62
  }
56
63
  }