js-draw 0.0.10 → 0.1.2

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 (122) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Editor.d.ts +2 -2
  4. package/dist/src/Editor.js +17 -7
  5. package/dist/src/EditorImage.d.ts +15 -7
  6. package/dist/src/EditorImage.js +46 -37
  7. package/dist/src/Pointer.d.ts +3 -2
  8. package/dist/src/Pointer.js +12 -3
  9. package/dist/src/SVGLoader.d.ts +6 -2
  10. package/dist/src/SVGLoader.js +20 -8
  11. package/dist/src/Viewport.d.ts +4 -0
  12. package/dist/src/Viewport.js +51 -0
  13. package/dist/src/components/AbstractComponent.d.ts +9 -2
  14. package/dist/src/components/AbstractComponent.js +14 -0
  15. package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -1
  16. package/dist/src/components/SVGGlobalAttributesObject.js +1 -1
  17. package/dist/src/components/Stroke.d.ts +1 -1
  18. package/dist/src/components/Stroke.js +1 -1
  19. package/dist/src/components/UnknownSVGObject.d.ts +1 -1
  20. package/dist/src/components/UnknownSVGObject.js +1 -1
  21. package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
  22. package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -1
  23. package/dist/src/components/builders/FreehandLineBuilder.js +1 -1
  24. package/dist/src/components/builders/LineBuilder.d.ts +1 -1
  25. package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
  26. package/dist/src/components/builders/types.d.ts +1 -1
  27. package/dist/src/geometry/Mat33.js +3 -0
  28. package/dist/src/geometry/Path.d.ts +1 -1
  29. package/dist/src/geometry/Path.js +102 -69
  30. package/dist/src/geometry/Rect2.d.ts +1 -0
  31. package/dist/src/geometry/Rect2.js +47 -9
  32. package/dist/src/{Display.d.ts → rendering/Display.d.ts} +5 -2
  33. package/dist/src/{Display.js → rendering/Display.js} +34 -4
  34. package/dist/src/rendering/caching/CacheRecord.d.ts +19 -0
  35. package/dist/src/rendering/caching/CacheRecord.js +52 -0
  36. package/dist/src/rendering/caching/CacheRecordManager.d.ts +11 -0
  37. package/dist/src/rendering/caching/CacheRecordManager.js +31 -0
  38. package/dist/src/rendering/caching/RenderingCache.d.ts +12 -0
  39. package/dist/src/rendering/caching/RenderingCache.js +42 -0
  40. package/dist/src/rendering/caching/RenderingCacheNode.d.ts +28 -0
  41. package/dist/src/rendering/caching/RenderingCacheNode.js +301 -0
  42. package/dist/src/rendering/caching/testUtils.d.ts +9 -0
  43. package/dist/src/rendering/caching/testUtils.js +20 -0
  44. package/dist/src/rendering/caching/types.d.ts +21 -0
  45. package/dist/src/rendering/caching/types.js +1 -0
  46. package/dist/src/rendering/{AbstractRenderer.d.ts → renderers/AbstractRenderer.d.ts} +20 -9
  47. package/dist/src/rendering/{AbstractRenderer.js → renderers/AbstractRenderer.js} +37 -3
  48. package/dist/src/rendering/{CanvasRenderer.d.ts → renderers/CanvasRenderer.d.ts} +10 -5
  49. package/dist/src/rendering/{CanvasRenderer.js → renderers/CanvasRenderer.js} +60 -20
  50. package/dist/src/rendering/{DummyRenderer.d.ts → renderers/DummyRenderer.d.ts} +9 -5
  51. package/dist/src/rendering/{DummyRenderer.js → renderers/DummyRenderer.js} +35 -4
  52. package/dist/src/rendering/{SVGRenderer.d.ts → renderers/SVGRenderer.d.ts} +7 -5
  53. package/dist/src/rendering/{SVGRenderer.js → renderers/SVGRenderer.js} +35 -18
  54. package/dist/src/testing/createEditor.js +1 -1
  55. package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
  56. package/dist/src/toolbar/HTMLToolbar.js +165 -154
  57. package/dist/src/toolbar/icons.d.ts +10 -0
  58. package/dist/src/toolbar/icons.js +180 -0
  59. package/dist/src/toolbar/localization.d.ts +4 -1
  60. package/dist/src/toolbar/localization.js +4 -1
  61. package/dist/src/toolbar/types.d.ts +4 -0
  62. package/dist/src/tools/PanZoom.d.ts +9 -6
  63. package/dist/src/tools/PanZoom.js +30 -21
  64. package/dist/src/tools/Pen.js +8 -3
  65. package/dist/src/tools/SelectionTool.js +9 -24
  66. package/dist/src/tools/ToolController.d.ts +5 -6
  67. package/dist/src/tools/ToolController.js +8 -10
  68. package/dist/src/tools/localization.d.ts +1 -0
  69. package/dist/src/tools/localization.js +1 -0
  70. package/dist/src/types.d.ts +2 -1
  71. package/package.json +1 -1
  72. package/src/Editor.ts +19 -8
  73. package/src/EditorImage.test.ts +2 -2
  74. package/src/EditorImage.ts +58 -42
  75. package/src/Pointer.ts +13 -4
  76. package/src/SVGLoader.ts +36 -10
  77. package/src/Viewport.ts +68 -0
  78. package/src/components/AbstractComponent.ts +21 -2
  79. package/src/components/SVGGlobalAttributesObject.ts +2 -2
  80. package/src/components/Stroke.ts +2 -2
  81. package/src/components/UnknownSVGObject.ts +2 -2
  82. package/src/components/builders/ArrowBuilder.ts +1 -1
  83. package/src/components/builders/FreehandLineBuilder.ts +2 -2
  84. package/src/components/builders/LineBuilder.ts +1 -1
  85. package/src/components/builders/RectangleBuilder.ts +1 -1
  86. package/src/components/builders/types.ts +1 -1
  87. package/src/geometry/Mat33.ts +3 -0
  88. package/src/geometry/Path.fromString.test.ts +94 -4
  89. package/src/geometry/Path.toString.test.ts +12 -2
  90. package/src/geometry/Path.ts +107 -71
  91. package/src/geometry/Rect2.test.ts +47 -8
  92. package/src/geometry/Rect2.ts +57 -9
  93. package/src/{Display.ts → rendering/Display.ts} +39 -6
  94. package/src/rendering/caching/CacheRecord.test.ts +49 -0
  95. package/src/rendering/caching/CacheRecord.ts +73 -0
  96. package/src/rendering/caching/CacheRecordManager.ts +45 -0
  97. package/src/rendering/caching/RenderingCache.test.ts +44 -0
  98. package/src/rendering/caching/RenderingCache.ts +63 -0
  99. package/src/rendering/caching/RenderingCacheNode.ts +378 -0
  100. package/src/rendering/caching/testUtils.ts +35 -0
  101. package/src/rendering/caching/types.ts +39 -0
  102. package/src/rendering/{AbstractRenderer.ts → renderers/AbstractRenderer.ts} +57 -9
  103. package/src/rendering/{CanvasRenderer.ts → renderers/CanvasRenderer.ts} +74 -25
  104. package/src/rendering/renderers/DummyRenderer.test.ts +43 -0
  105. package/src/rendering/{DummyRenderer.ts → renderers/DummyRenderer.ts} +50 -7
  106. package/src/rendering/{SVGRenderer.ts → renderers/SVGRenderer.ts} +39 -23
  107. package/src/testing/createEditor.ts +1 -1
  108. package/src/toolbar/HTMLToolbar.ts +199 -170
  109. package/src/toolbar/icons.ts +203 -0
  110. package/src/toolbar/localization.ts +9 -2
  111. package/src/toolbar/toolbar.css +21 -8
  112. package/src/toolbar/types.ts +5 -0
  113. package/src/tools/PanZoom.ts +37 -27
  114. package/src/tools/Pen.ts +7 -3
  115. package/src/tools/SelectionTool.test.ts +1 -1
  116. package/src/tools/SelectionTool.ts +12 -33
  117. package/src/tools/ToolController.ts +3 -5
  118. package/src/tools/localization.ts +2 -0
  119. package/src/types.ts +10 -3
  120. package/tsconfig.json +1 -0
  121. package/dist/__mocks__/coloris.d.ts +0 -2
  122. package/dist/__mocks__/coloris.js +0 -5
@@ -41,7 +41,7 @@ export default class Stroke extends AbstractComponent {
41
41
  canvas.drawPath(part);
42
42
  }
43
43
  }
44
- canvas.endObject();
44
+ canvas.endObject(this.getLoadSaveData());
45
45
  }
46
46
  // Grows the bounding box for a given stroke part based on that part's style.
47
47
  bboxForPart(origBBox, style) {
@@ -1,7 +1,7 @@
1
1
  import LineSegment2 from '../geometry/LineSegment2';
2
2
  import Mat33 from '../geometry/Mat33';
3
3
  import Rect2 from '../geometry/Rect2';
4
- import AbstractRenderer from '../rendering/AbstractRenderer';
4
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
5
  import AbstractComponent from './AbstractComponent';
6
6
  import { ImageComponentLocalization } from './localization';
7
7
  export default class UnknownSVGObject extends AbstractComponent {
@@ -1,5 +1,5 @@
1
1
  import Rect2 from '../geometry/Rect2';
2
- import SVGRenderer from '../rendering/SVGRenderer';
2
+ import SVGRenderer from '../rendering/renderers/SVGRenderer';
3
3
  import AbstractComponent from './AbstractComponent';
4
4
  export default class UnknownSVGObject extends AbstractComponent {
5
5
  constructor(svgObject) {
@@ -1,5 +1,5 @@
1
1
  import Rect2 from '../../geometry/Rect2';
2
- import AbstractRenderer from '../../rendering/AbstractRenderer';
2
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
3
3
  import { StrokeDataPoint } from '../../types';
4
4
  import AbstractComponent from '../AbstractComponent';
5
5
  import { ComponentBuilder, ComponentBuilderFactory } from './types';
@@ -1,4 +1,4 @@
1
- import AbstractRenderer from '../../rendering/AbstractRenderer';
1
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
2
2
  import Rect2 from '../../geometry/Rect2';
3
3
  import Stroke from '../Stroke';
4
4
  import { StrokeDataPoint } from '../../types';
@@ -160,7 +160,7 @@ export default class FreehandLineBuilder {
160
160
  const upperBoundary = computeBoundaryCurve(1, halfVec);
161
161
  const lowerBoundary = computeBoundaryCurve(-1, halfVec);
162
162
  // If the boundaries have two intersections, increasing the half vector's length could fix this.
163
- if (upperBoundary.intersects(lowerBoundary).length === 2) {
163
+ if (upperBoundary.intersects(lowerBoundary).length > 0) {
164
164
  halfVec = halfVec.times(2);
165
165
  }
166
166
  const pathCommands = [
@@ -1,5 +1,5 @@
1
1
  import Rect2 from '../../geometry/Rect2';
2
- import AbstractRenderer from '../../rendering/AbstractRenderer';
2
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
3
3
  import { StrokeDataPoint } from '../../types';
4
4
  import AbstractComponent from '../AbstractComponent';
5
5
  import { ComponentBuilder, ComponentBuilderFactory } from './types';
@@ -1,5 +1,5 @@
1
1
  import Rect2 from '../../geometry/Rect2';
2
- import AbstractRenderer from '../../rendering/AbstractRenderer';
2
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
3
3
  import { StrokeDataPoint } from '../../types';
4
4
  import AbstractComponent from '../AbstractComponent';
5
5
  import { ComponentBuilder, ComponentBuilderFactory } from './types';
@@ -1,5 +1,5 @@
1
1
  import Rect2 from '../../geometry/Rect2';
2
- import AbstractRenderer from '../../rendering/AbstractRenderer';
2
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
3
3
  import { StrokeDataPoint } from '../../types';
4
4
  import Viewport from '../../Viewport';
5
5
  import AbstractComponent from '../AbstractComponent';
@@ -4,6 +4,9 @@ import Vec3 from './Vec3';
4
4
  // a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
5
5
  // **and** translates while a linear transformation just scales/rotates/shears).
6
6
  export default class Mat33 {
7
+ // ⎡ a1 a2 a3 ⎤
8
+ // ⎢ b1 b2 b3 ⎥
9
+ // ⎣ c1 c2 c3 ⎦
7
10
  constructor(a1, a2, a3, b1, b2, b3, c1, c2, c3) {
8
11
  this.a1 = a1;
9
12
  this.a2 = a2;
@@ -1,5 +1,5 @@
1
1
  import { Bezier } from 'bezier-js';
2
- import { RenderingStyle, RenderablePathSpec } from '../rendering/AbstractRenderer';
2
+ import { RenderingStyle, RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
3
3
  import LineSegment2 from './LineSegment2';
4
4
  import Mat33 from './Mat33';
5
5
  import Rect2 from './Rect2';
@@ -208,8 +208,8 @@ export default class Path {
208
208
  const toRoundedString = (num) => {
209
209
  // Try to remove rounding errors. If the number ends in at least three/four zeroes
210
210
  // (or nines) just one or two digits, it's probably a rounding error.
211
- const fixRoundingUpExp = /^([-]?\d*\.?\d*[1-9.])0{4,}\d$/;
212
- const hasRoundingDownExp = /^([-]?)(\d*)\.(\d*9{4,}\d)$/;
211
+ const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d$/;
212
+ const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,}\d)$/;
213
213
  let text = num.toString();
214
214
  if (text.indexOf('.') === -1) {
215
215
  return text;
@@ -230,7 +230,9 @@ export default class Path {
230
230
  text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
231
231
  }
232
232
  text = text.replace(fixRoundingUpExp, '$1');
233
- // Remove trailing period (if it exists)
233
+ // Remove trailing zeroes
234
+ text = text.replace(/([.][^0]*)0+$/, '$1');
235
+ // Remove trailing period
234
236
  return text.replace(/[.]$/, '');
235
237
  };
236
238
  const addCommand = (command, ...points) => {
@@ -273,10 +275,12 @@ export default class Path {
273
275
  // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
274
276
  // and
275
277
  // https://www.w3.org/TR/SVG2/paths.html
278
+ var _a;
276
279
  // Remove linebreaks
277
280
  pathString = pathString.split('\n').join(' ');
278
281
  let lastPos = Vec2.zero;
279
282
  let firstPos = null;
283
+ let startPos = null;
280
284
  let isFirstCommand = true;
281
285
  const commands = [];
282
286
  const moveTo = (point) => {
@@ -315,15 +319,61 @@ export default class Path {
315
319
  endPoint,
316
320
  });
317
321
  };
322
+ const commandArgCounts = {
323
+ 'm': 1,
324
+ 'l': 1,
325
+ 'c': 3,
326
+ 'q': 2,
327
+ 'z': 0,
328
+ 'h': 1,
329
+ 'v': 1,
330
+ };
318
331
  // Each command: Command character followed by anything that isn't a command character
319
- const commandExp = /([MmZzLlHhVvCcSsQqTtAa])\s*([^a-zA-Z]*)/g;
332
+ const commandExp = /([MZLHVCSQTA])\s*([^MZLHVCSQTA]*)/ig;
320
333
  let current;
321
334
  while ((current = commandExp.exec(pathString)) !== null) {
322
- const argParts = current[2].trim().split(/[^0-9.-]/).filter(part => part.length > 0);
323
- const numericArgs = argParts.map(arg => parseFloat(arg));
324
- const commandChar = current[1];
325
- const uppercaseCommand = commandChar !== commandChar.toLowerCase();
326
- const args = numericArgs.reduce((accumulator, current, index, parts) => {
335
+ const argParts = current[2].trim().split(/[^0-9Ee.-]/).filter(part => part.length > 0).reduce((accumualtor, current) => {
336
+ // As of 09/2022, iOS Safari doesn't support support lookbehind in regular
337
+ // expressions. As such, we need an alternative.
338
+ // Because '-' can be used as a path separator, unless preceeded by an 'e' (as in 1e-5),
339
+ // we need special cases:
340
+ current = current.replace(/([^eE])[-]/g, '$1 -');
341
+ const parts = current.split(' -');
342
+ if (parts[0] !== '') {
343
+ accumualtor.push(parts[0]);
344
+ }
345
+ accumualtor.push(...parts.slice(1).map(part => `-${part}`));
346
+ return accumualtor;
347
+ }, []);
348
+ let numericArgs = argParts.map(arg => parseFloat(arg));
349
+ let commandChar = current[1].toLowerCase();
350
+ let uppercaseCommand = current[1] !== commandChar;
351
+ // Convert commands that don't take points into commands that do.
352
+ if (commandChar === 'v' || commandChar === 'h') {
353
+ numericArgs = numericArgs.reduce((accumulator, current) => {
354
+ if (commandChar === 'v') {
355
+ return accumulator.concat(uppercaseCommand ? lastPos.x : 0, current);
356
+ }
357
+ else {
358
+ return accumulator.concat(current, uppercaseCommand ? lastPos.y : 0);
359
+ }
360
+ }, []);
361
+ commandChar = 'l';
362
+ }
363
+ else if (commandChar === 'z') {
364
+ if (firstPos) {
365
+ numericArgs = [firstPos.x, firstPos.y];
366
+ firstPos = lastPos;
367
+ }
368
+ else {
369
+ continue;
370
+ }
371
+ // 'z' always acts like an uppercase lineTo(startPos)
372
+ uppercaseCommand = true;
373
+ commandChar = 'l';
374
+ }
375
+ const commandArgCount = (_a = commandArgCounts[commandChar]) !== null && _a !== void 0 ? _a : 0;
376
+ const allArgs = numericArgs.reduce((accumulator, current, index, parts) => {
327
377
  if (index % 2 !== 0) {
328
378
  const currentAsFloat = current;
329
379
  const prevAsFloat = parts[index - 1];
@@ -332,76 +382,59 @@ export default class Path {
332
382
  else {
333
383
  return accumulator;
334
384
  }
335
- }, []).map((coordinate) => {
385
+ }, []).map((coordinate, index) => {
336
386
  // Lowercase commands are relative, uppercase commands use absolute
337
387
  // positioning
388
+ let newPos;
338
389
  if (uppercaseCommand) {
339
- lastPos = coordinate;
340
- return coordinate;
390
+ newPos = coordinate;
341
391
  }
342
392
  else {
343
- lastPos = lastPos.plus(coordinate);
344
- return lastPos;
393
+ newPos = lastPos.plus(coordinate);
394
+ }
395
+ if ((index + 1) % commandArgCount === 0) {
396
+ lastPos = newPos;
345
397
  }
398
+ return newPos;
346
399
  });
347
- let expectedPointArgCount;
348
- switch (commandChar.toLowerCase()) {
349
- case 'm':
350
- expectedPointArgCount = 1;
351
- moveTo(args[0]);
352
- break;
353
- case 'l':
354
- expectedPointArgCount = 1;
355
- lineTo(args[0]);
356
- break;
357
- case 'z':
358
- expectedPointArgCount = 0;
359
- // firstPos can be null if the stroke data is just 'z'.
360
- if (firstPos) {
361
- lineTo(firstPos);
362
- }
363
- break;
364
- case 'c':
365
- expectedPointArgCount = 3;
366
- cubicBezierTo(args[0], args[1], args[2]);
367
- break;
368
- case 'q':
369
- expectedPointArgCount = 2;
370
- quadraticBeierTo(args[0], args[1]);
371
- break;
372
- // Horizontal line
373
- case 'h':
374
- expectedPointArgCount = 0;
375
- if (uppercaseCommand) {
376
- lineTo(Vec2.of(numericArgs[0], lastPos.y));
377
- }
378
- else {
379
- lineTo(lastPos.plus(Vec2.of(numericArgs[0], 0)));
380
- }
381
- break;
382
- // Vertical line
383
- case 'v':
384
- expectedPointArgCount = 0;
385
- if (uppercaseCommand) {
386
- lineTo(Vec2.of(lastPos.x, numericArgs[1]));
387
- }
388
- else {
389
- lineTo(lastPos.plus(Vec2.of(0, numericArgs[1])));
390
- }
391
- break;
392
- default:
393
- throw new Error(`Unknown path command ${commandChar}`);
400
+ if (allArgs.length % commandArgCount !== 0) {
401
+ throw new Error([
402
+ `Incorrect number of arguments: got ${JSON.stringify(allArgs)} with a length of ${allArgs.length} ≠ ${commandArgCount}k, k ∈ ℤ.`,
403
+ `The number of arguments to ${commandChar} must be a multiple of ${commandArgCount}!`,
404
+ `Command: ${current[0]}`,
405
+ ].join('\n'));
394
406
  }
395
- if (args.length !== expectedPointArgCount) {
396
- throw new Error(`
397
- Incorrect number of arguments: got ${JSON.stringify(args)} with a length of ${args.length} ≠ ${expectedPointArgCount}.
398
- `.trim());
407
+ for (let argPos = 0; argPos < allArgs.length; argPos += commandArgCount) {
408
+ const args = allArgs.slice(argPos, argPos + commandArgCount);
409
+ switch (commandChar.toLowerCase()) {
410
+ case 'm':
411
+ if (argPos === 0) {
412
+ moveTo(args[0]);
413
+ }
414
+ else {
415
+ lineTo(args[0]);
416
+ }
417
+ break;
418
+ case 'l':
419
+ lineTo(args[0]);
420
+ break;
421
+ case 'c':
422
+ cubicBezierTo(args[0], args[1], args[2]);
423
+ break;
424
+ case 'q':
425
+ quadraticBeierTo(args[0], args[1]);
426
+ break;
427
+ default:
428
+ throw new Error(`Unknown path command ${commandChar}`);
429
+ }
430
+ isFirstCommand = false;
399
431
  }
400
- if (args.length > 0) {
401
- firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos = args[0]);
432
+ if (allArgs.length > 0) {
433
+ firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos = allArgs[0]);
434
+ startPos !== null && startPos !== void 0 ? startPos : (startPos = firstPos);
435
+ lastPos = allArgs[allArgs.length - 1];
402
436
  }
403
- isFirstCommand = false;
404
437
  }
405
- return new Path(firstPos !== null && firstPos !== void 0 ? firstPos : Vec2.zero, commands);
438
+ return new Path(startPos !== null && startPos !== void 0 ? startPos : Vec2.zero, commands);
406
439
  }
407
440
  }
@@ -27,6 +27,7 @@ export default class Rect2 {
27
27
  intersects(other: Rect2): boolean;
28
28
  intersection(other: Rect2): Rect2 | null;
29
29
  union(other: Rect2): Rect2;
30
+ divideIntoGrid(columns: number, rows: number): Rect2[];
30
31
  grownToPoint(point: Point2, margin?: number): Rect2;
31
32
  grownBy(margin: number): Rect2;
32
33
  get corners(): Point2[];
@@ -38,20 +38,31 @@ export default class Rect2 {
38
38
  && this.bottomRight.y >= other.bottomRight.y;
39
39
  }
40
40
  intersects(other) {
41
- return this.intersection(other) !== null;
41
+ // Project along x/y axes.
42
+ const thisMinX = this.x;
43
+ const thisMaxX = thisMinX + this.w;
44
+ const otherMinX = other.x;
45
+ const otherMaxX = other.x + other.w;
46
+ if (thisMaxX < otherMinX || thisMinX > otherMaxX) {
47
+ return false;
48
+ }
49
+ const thisMinY = this.y;
50
+ const thisMaxY = thisMinY + this.h;
51
+ const otherMinY = other.y;
52
+ const otherMaxY = other.y + other.h;
53
+ if (thisMaxY < otherMinY || thisMinY > otherMaxY) {
54
+ return false;
55
+ }
56
+ return true;
42
57
  }
43
58
  // Returns the overlap of this and [other], or null, if no such
44
- // / overlap exists
59
+ // overlap exists
45
60
  intersection(other) {
46
- const topLeft = this.topLeft.zip(other.topLeft, Math.max);
47
- const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
48
- // The intersection can't be outside of this rectangle
49
- if (!this.containsPoint(topLeft) || !this.containsPoint(bottomRight)) {
50
- return null;
51
- }
52
- else if (!other.containsPoint(topLeft) || !other.containsPoint(bottomRight)) {
61
+ if (!this.intersects(other)) {
53
62
  return null;
54
63
  }
64
+ const topLeft = this.topLeft.zip(other.topLeft, Math.max);
65
+ const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
55
66
  return Rect2.fromCorners(topLeft, bottomRight);
56
67
  }
57
68
  // Returns a new rectangle containing both [this] and [other].
@@ -60,6 +71,33 @@ export default class Rect2 {
60
71
  const bottomRight = this.bottomRight.zip(other.bottomRight, Math.max);
61
72
  return Rect2.fromCorners(topLeft, bottomRight);
62
73
  }
74
+ // Returns a the subdivision of this into [columns] columns
75
+ // and [rows] rows. For example,
76
+ // Rect2.unitSquare.divideIntoGrid(2, 2)
77
+ // -> [ Rect2(0, 0, 0.5, 0.5), Rect2(0.5, 0, 0.5, 0.5), Rect2(0, 0.5, 0.5, 0.5), Rect2(0.5, 0.5, 0.5, 0.5) ]
78
+ // The rectangles are ordered in row-major order.
79
+ divideIntoGrid(columns, rows) {
80
+ const result = [];
81
+ if (columns <= 0 || rows <= 0) {
82
+ return result;
83
+ }
84
+ const eachRectWidth = this.w / columns;
85
+ const eachRectHeight = this.h / rows;
86
+ if (eachRectWidth === 0) {
87
+ columns = 1;
88
+ }
89
+ if (eachRectHeight === 0) {
90
+ rows = 1;
91
+ }
92
+ for (let j = 0; j < rows; j++) {
93
+ for (let i = 0; i < columns; i++) {
94
+ const x = eachRectWidth * i + this.x;
95
+ const y = eachRectHeight * j + this.y;
96
+ result.push(new Rect2(x, y, eachRectWidth, eachRectHeight));
97
+ }
98
+ }
99
+ return result;
100
+ }
63
101
  // Returns a rectangle containing this and [point].
64
102
  // [margin] is the minimum distance between the new point and the edge
65
103
  // of the resultant rectangle.
@@ -1,5 +1,6 @@
1
- import AbstractRenderer from './rendering/AbstractRenderer';
2
- import { Editor } from './Editor';
1
+ import AbstractRenderer from './renderers/AbstractRenderer';
2
+ import { Editor } from '../Editor';
3
+ import RenderingCache from './caching/RenderingCache';
3
4
  export declare enum RenderingMode {
4
5
  DummyRenderer = 0,
5
6
  CanvasRenderer = 1
@@ -9,11 +10,13 @@ export default class Display {
9
10
  private parent;
10
11
  private dryInkRenderer;
11
12
  private wetInkRenderer;
13
+ private cache;
12
14
  private resizeSurfacesCallback?;
13
15
  private flattenCallback?;
14
16
  constructor(editor: Editor, mode: RenderingMode, parent: HTMLElement | null);
15
17
  get width(): number;
16
18
  get height(): number;
19
+ getCache(): RenderingCache;
17
20
  private initializeCanvasRendering;
18
21
  startRerender(): AbstractRenderer;
19
22
  setDraftMode(draftMode: boolean): void;
@@ -1,7 +1,8 @@
1
- import CanvasRenderer from './rendering/CanvasRenderer';
2
- import { EditorEventType } from './types';
3
- import DummyRenderer from './rendering/DummyRenderer';
4
- import { Vec2 } from './geometry/Vec2';
1
+ import CanvasRenderer from './renderers/CanvasRenderer';
2
+ import { EditorEventType } from '../types';
3
+ import DummyRenderer from './renderers/DummyRenderer';
4
+ import { Vec2 } from '../geometry/Vec2';
5
+ import RenderingCache from './caching/RenderingCache';
5
6
  export var RenderingMode;
6
7
  (function (RenderingMode) {
7
8
  RenderingMode[RenderingMode["DummyRenderer"] = 0] = "DummyRenderer";
@@ -22,6 +23,32 @@ export default class Display {
22
23
  else {
23
24
  throw new Error(`Unknown rendering mode, ${mode}!`);
24
25
  }
26
+ const cacheBlockResolution = Vec2.of(600, 600);
27
+ this.cache = new RenderingCache({
28
+ createRenderer: () => {
29
+ if (mode === RenderingMode.DummyRenderer) {
30
+ return new DummyRenderer(editor.viewport);
31
+ }
32
+ else if (mode !== RenderingMode.CanvasRenderer) {
33
+ throw new Error('Unspported rendering mode');
34
+ }
35
+ // Make the canvas slightly larger than each cache block to prevent
36
+ // seams.
37
+ const canvas = document.createElement('canvas');
38
+ canvas.width = cacheBlockResolution.x + 1;
39
+ canvas.height = cacheBlockResolution.y + 1;
40
+ const ctx = canvas.getContext('2d');
41
+ return new CanvasRenderer(ctx, editor.viewport);
42
+ },
43
+ isOfCorrectType: (renderer) => {
44
+ return this.dryInkRenderer.canRenderFromWithoutDataLoss(renderer);
45
+ },
46
+ blockResolution: cacheBlockResolution,
47
+ cacheSize: 500 * 500 * 4 * 200,
48
+ maxScale: 1.5,
49
+ minComponentsPerCache: 50,
50
+ minComponentsToUseCache: 120,
51
+ });
25
52
  this.editor.notifier.on(EditorEventType.DisplayResized, event => {
26
53
  var _a;
27
54
  if (event.kind !== EditorEventType.DisplayResized) {
@@ -39,6 +66,9 @@ export default class Display {
39
66
  get height() {
40
67
  return this.dryInkRenderer.displaySize().y;
41
68
  }
69
+ getCache() {
70
+ return this.cache;
71
+ }
42
72
  initializeCanvasRendering() {
43
73
  const dryInkCanvas = document.createElement('canvas');
44
74
  const wetInkCanvas = document.createElement('canvas');
@@ -0,0 +1,19 @@
1
+ import Mat33 from '../../geometry/Mat33';
2
+ import Rect2 from '../../geometry/Rect2';
3
+ import AbstractRenderer from '../renderers/AbstractRenderer';
4
+ import { BeforeDeallocCallback, CacheState } from './types';
5
+ export default class CacheRecord {
6
+ private onBeforeDeallocCallback;
7
+ private cacheState;
8
+ private renderer;
9
+ private lastUsedCycle;
10
+ private allocd;
11
+ constructor(onBeforeDeallocCallback: BeforeDeallocCallback | null, cacheState: CacheState);
12
+ startRender(): AbstractRenderer;
13
+ dealloc(): void;
14
+ isAllocd(): boolean;
15
+ realloc(newDeallocCallback: BeforeDeallocCallback): void;
16
+ getLastUsedCycle(): number;
17
+ getTransform(drawTo: Rect2): Mat33;
18
+ setRenderingRegion(drawTo: Rect2): void;
19
+ }
@@ -0,0 +1,52 @@
1
+ import Mat33 from '../../geometry/Mat33';
2
+ // Represents a cached renderer/canvas
3
+ // This is not a [CacheNode] -- it handles cached renderers and does not have sub-renderers.
4
+ export default class CacheRecord {
5
+ constructor(onBeforeDeallocCallback, cacheState) {
6
+ this.onBeforeDeallocCallback = onBeforeDeallocCallback;
7
+ this.cacheState = cacheState;
8
+ this.allocd = false;
9
+ this.renderer = cacheState.props.createRenderer();
10
+ this.lastUsedCycle = -1;
11
+ this.allocd = true;
12
+ }
13
+ startRender() {
14
+ this.lastUsedCycle = this.cacheState.currentRenderingCycle;
15
+ if (!this.allocd) {
16
+ throw new Error('Only alloc\'d canvases can be rendered to');
17
+ }
18
+ return this.renderer;
19
+ }
20
+ dealloc() {
21
+ var _a;
22
+ (_a = this.onBeforeDeallocCallback) === null || _a === void 0 ? void 0 : _a.call(this);
23
+ this.allocd = false;
24
+ this.onBeforeDeallocCallback = null;
25
+ this.lastUsedCycle = 0;
26
+ }
27
+ isAllocd() {
28
+ return this.allocd;
29
+ }
30
+ realloc(newDeallocCallback) {
31
+ if (this.allocd) {
32
+ this.dealloc();
33
+ }
34
+ this.allocd = true;
35
+ this.onBeforeDeallocCallback = newDeallocCallback;
36
+ this.lastUsedCycle = this.cacheState.currentRenderingCycle;
37
+ }
38
+ getLastUsedCycle() {
39
+ return this.lastUsedCycle;
40
+ }
41
+ // Returns the transformation that maps [drawTo] to this' renderable region
42
+ // (i.e. a [cacheProps.blockResolution]-sized rectangle with top left at (0, 0))
43
+ getTransform(drawTo) {
44
+ const transform = Mat33.scaling2D(this.cacheState.props.blockResolution.x / drawTo.size.x).rightMul(Mat33.translation(drawTo.topLeft.times(-1)));
45
+ return transform;
46
+ }
47
+ setRenderingRegion(drawTo) {
48
+ this.renderer.setTransform(
49
+ // Invert to map objects instead of the viewport
50
+ this.getTransform(drawTo));
51
+ }
52
+ }
@@ -0,0 +1,11 @@
1
+ import { BeforeDeallocCallback, PartialCacheState } from './types';
2
+ import CacheRecord from './CacheRecord';
3
+ import Rect2 from '../../geometry/Rect2';
4
+ export declare class CacheRecordManager {
5
+ private readonly cacheState;
6
+ private cacheRecords;
7
+ private maxCanvases;
8
+ constructor(cacheState: PartialCacheState);
9
+ allocCanvas(drawTo: Rect2, onDealloc: BeforeDeallocCallback): CacheRecord;
10
+ private getLeastRecentlyUsedRecord;
11
+ }
@@ -0,0 +1,31 @@
1
+ import CacheRecord from './CacheRecord';
2
+ export class CacheRecordManager {
3
+ constructor(cacheState) {
4
+ this.cacheState = cacheState;
5
+ // Fixed-size array: Cache blocks are assigned indicies into [cachedCanvases].
6
+ this.cacheRecords = [];
7
+ const cacheProps = cacheState.props;
8
+ this.maxCanvases = Math.ceil(
9
+ // Assuming four components per pixel:
10
+ cacheProps.cacheSize / 4 / cacheProps.blockResolution.x / cacheProps.blockResolution.y);
11
+ }
12
+ allocCanvas(drawTo, onDealloc) {
13
+ if (this.cacheRecords.length < this.maxCanvases) {
14
+ const record = new CacheRecord(onDealloc, Object.assign(Object.assign({}, this.cacheState), { recordManager: this }));
15
+ record.setRenderingRegion(drawTo);
16
+ this.cacheRecords.push(record);
17
+ return record;
18
+ }
19
+ else {
20
+ const lru = this.getLeastRecentlyUsedRecord();
21
+ lru.realloc(onDealloc);
22
+ lru.setRenderingRegion(drawTo);
23
+ return lru;
24
+ }
25
+ }
26
+ // Returns null if there are no cache records. Returns an unalloc'd record if one exists.
27
+ getLeastRecentlyUsedRecord() {
28
+ this.cacheRecords.sort((a, b) => a.getLastUsedCycle() - b.getLastUsedCycle());
29
+ return this.cacheRecords[0];
30
+ }
31
+ }
@@ -0,0 +1,12 @@
1
+ import { ImageNode } from '../../EditorImage';
2
+ import Viewport from '../../Viewport';
3
+ import AbstractRenderer from '../renderers/AbstractRenderer';
4
+ import { CacheProps, CacheState } from './types';
5
+ export default class RenderingCache {
6
+ private partialSharedState;
7
+ private recordManager;
8
+ private rootNode;
9
+ constructor(cacheProps: CacheProps);
10
+ getSharedState(): CacheState;
11
+ render(screenRenderer: AbstractRenderer, image: ImageNode, viewport: Viewport): void;
12
+ }