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
@@ -220,6 +220,7 @@ export default class Path {
220
220
  const lastDigit = parseInt(text.charAt(text.length - 1), 10);
221
221
  const postDecimal = parseInt(roundingDownMatch[3], 10);
222
222
  const preDecimal = parseInt(roundingDownMatch[2], 10);
223
+ const origPostDecimalString = roundingDownMatch[3];
223
224
  let newPostDecimal = (postDecimal + 10 - lastDigit).toString();
224
225
  let carry = 0;
225
226
  if (newPostDecimal.length > postDecimal.toString().length) {
@@ -227,11 +228,17 @@ export default class Path {
227
228
  newPostDecimal = newPostDecimal.substring(1);
228
229
  carry = 1;
229
230
  }
231
+ // parseInt(...).toString() removes leading zeroes. Add them back.
232
+ while (newPostDecimal.length < origPostDecimalString.length) {
233
+ newPostDecimal = carry.toString(10) + newPostDecimal;
234
+ carry = 0;
235
+ }
230
236
  text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
231
237
  }
232
238
  text = text.replace(fixRoundingUpExp, '$1');
233
239
  // Remove trailing zeroes
234
- text = text.replace(/([.][^0]*)0+$/, '$1');
240
+ text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
241
+ text = text.replace(/[.]0+$/, '.');
235
242
  // Remove trailing period
236
243
  return text.replace(/[.]$/, '');
237
244
  };
@@ -275,10 +282,12 @@ export default class Path {
275
282
  // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/d
276
283
  // and
277
284
  // https://www.w3.org/TR/SVG2/paths.html
285
+ var _a;
278
286
  // Remove linebreaks
279
287
  pathString = pathString.split('\n').join(' ');
280
288
  let lastPos = Vec2.zero;
281
289
  let firstPos = null;
290
+ let startPos = null;
282
291
  let isFirstCommand = true;
283
292
  const commands = [];
284
293
  const moveTo = (point) => {
@@ -317,15 +326,61 @@ export default class Path {
317
326
  endPoint,
318
327
  });
319
328
  };
329
+ const commandArgCounts = {
330
+ 'm': 1,
331
+ 'l': 1,
332
+ 'c': 3,
333
+ 'q': 2,
334
+ 'z': 0,
335
+ 'h': 1,
336
+ 'v': 1,
337
+ };
320
338
  // Each command: Command character followed by anything that isn't a command character
321
- const commandExp = /([MmZzLlHhVvCcSsQqTtAa])\s*([^a-zA-Z]*)/g;
339
+ const commandExp = /([MZLHVCSQTA])\s*([^MZLHVCSQTA]*)/ig;
322
340
  let current;
323
341
  while ((current = commandExp.exec(pathString)) !== null) {
324
- const argParts = current[2].trim().split(/[^0-9.-]/).filter(part => part.length > 0);
325
- const numericArgs = argParts.map(arg => parseFloat(arg));
326
- const commandChar = current[1];
327
- const uppercaseCommand = commandChar !== commandChar.toLowerCase();
328
- const args = numericArgs.reduce((accumulator, current, index, parts) => {
342
+ const argParts = current[2].trim().split(/[^0-9Ee.-]/).filter(part => part.length > 0).reduce((accumualtor, current) => {
343
+ // As of 09/2022, iOS Safari doesn't support support lookbehind in regular
344
+ // expressions. As such, we need an alternative.
345
+ // Because '-' can be used as a path separator, unless preceeded by an 'e' (as in 1e-5),
346
+ // we need special cases:
347
+ current = current.replace(/([^eE])[-]/g, '$1 -');
348
+ const parts = current.split(' -');
349
+ if (parts[0] !== '') {
350
+ accumualtor.push(parts[0]);
351
+ }
352
+ accumualtor.push(...parts.slice(1).map(part => `-${part}`));
353
+ return accumualtor;
354
+ }, []);
355
+ let numericArgs = argParts.map(arg => parseFloat(arg));
356
+ let commandChar = current[1].toLowerCase();
357
+ let uppercaseCommand = current[1] !== commandChar;
358
+ // Convert commands that don't take points into commands that do.
359
+ if (commandChar === 'v' || commandChar === 'h') {
360
+ numericArgs = numericArgs.reduce((accumulator, current) => {
361
+ if (commandChar === 'v') {
362
+ return accumulator.concat(uppercaseCommand ? lastPos.x : 0, current);
363
+ }
364
+ else {
365
+ return accumulator.concat(current, uppercaseCommand ? lastPos.y : 0);
366
+ }
367
+ }, []);
368
+ commandChar = 'l';
369
+ }
370
+ else if (commandChar === 'z') {
371
+ if (firstPos) {
372
+ numericArgs = [firstPos.x, firstPos.y];
373
+ firstPos = lastPos;
374
+ }
375
+ else {
376
+ continue;
377
+ }
378
+ // 'z' always acts like an uppercase lineTo(startPos)
379
+ uppercaseCommand = true;
380
+ commandChar = 'l';
381
+ }
382
+ const commandArgCount = (_a = commandArgCounts[commandChar]) !== null && _a !== void 0 ? _a : 0;
383
+ const allArgs = numericArgs.reduce((accumulator, current, index, parts) => {
329
384
  if (index % 2 !== 0) {
330
385
  const currentAsFloat = current;
331
386
  const prevAsFloat = parts[index - 1];
@@ -334,76 +389,59 @@ export default class Path {
334
389
  else {
335
390
  return accumulator;
336
391
  }
337
- }, []).map((coordinate) => {
392
+ }, []).map((coordinate, index) => {
338
393
  // Lowercase commands are relative, uppercase commands use absolute
339
394
  // positioning
395
+ let newPos;
340
396
  if (uppercaseCommand) {
341
- lastPos = coordinate;
342
- return coordinate;
397
+ newPos = coordinate;
343
398
  }
344
399
  else {
345
- lastPos = lastPos.plus(coordinate);
346
- return lastPos;
400
+ newPos = lastPos.plus(coordinate);
401
+ }
402
+ if ((index + 1) % commandArgCount === 0) {
403
+ lastPos = newPos;
347
404
  }
405
+ return newPos;
348
406
  });
349
- let expectedPointArgCount;
350
- switch (commandChar.toLowerCase()) {
351
- case 'm':
352
- expectedPointArgCount = 1;
353
- moveTo(args[0]);
354
- break;
355
- case 'l':
356
- expectedPointArgCount = 1;
357
- lineTo(args[0]);
358
- break;
359
- case 'z':
360
- expectedPointArgCount = 0;
361
- // firstPos can be null if the stroke data is just 'z'.
362
- if (firstPos) {
363
- lineTo(firstPos);
364
- }
365
- break;
366
- case 'c':
367
- expectedPointArgCount = 3;
368
- cubicBezierTo(args[0], args[1], args[2]);
369
- break;
370
- case 'q':
371
- expectedPointArgCount = 2;
372
- quadraticBeierTo(args[0], args[1]);
373
- break;
374
- // Horizontal line
375
- case 'h':
376
- expectedPointArgCount = 0;
377
- if (uppercaseCommand) {
378
- lineTo(Vec2.of(numericArgs[0], lastPos.y));
379
- }
380
- else {
381
- lineTo(lastPos.plus(Vec2.of(numericArgs[0], 0)));
382
- }
383
- break;
384
- // Vertical line
385
- case 'v':
386
- expectedPointArgCount = 0;
387
- if (uppercaseCommand) {
388
- lineTo(Vec2.of(lastPos.x, numericArgs[1]));
389
- }
390
- else {
391
- lineTo(lastPos.plus(Vec2.of(0, numericArgs[1])));
392
- }
393
- break;
394
- default:
395
- throw new Error(`Unknown path command ${commandChar}`);
407
+ if (allArgs.length % commandArgCount !== 0) {
408
+ throw new Error([
409
+ `Incorrect number of arguments: got ${JSON.stringify(allArgs)} with a length of ${allArgs.length} ≠ ${commandArgCount}k, k ∈ ℤ.`,
410
+ `The number of arguments to ${commandChar} must be a multiple of ${commandArgCount}!`,
411
+ `Command: ${current[0]}`,
412
+ ].join('\n'));
396
413
  }
397
- if (args.length !== expectedPointArgCount) {
398
- throw new Error(`
399
- Incorrect number of arguments: got ${JSON.stringify(args)} with a length of ${args.length} ≠ ${expectedPointArgCount}.
400
- `.trim());
414
+ for (let argPos = 0; argPos < allArgs.length; argPos += commandArgCount) {
415
+ const args = allArgs.slice(argPos, argPos + commandArgCount);
416
+ switch (commandChar.toLowerCase()) {
417
+ case 'm':
418
+ if (argPos === 0) {
419
+ moveTo(args[0]);
420
+ }
421
+ else {
422
+ lineTo(args[0]);
423
+ }
424
+ break;
425
+ case 'l':
426
+ lineTo(args[0]);
427
+ break;
428
+ case 'c':
429
+ cubicBezierTo(args[0], args[1], args[2]);
430
+ break;
431
+ case 'q':
432
+ quadraticBeierTo(args[0], args[1]);
433
+ break;
434
+ default:
435
+ throw new Error(`Unknown path command ${commandChar}`);
436
+ }
437
+ isFirstCommand = false;
401
438
  }
402
- if (args.length > 0) {
403
- firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos = args[0]);
439
+ if (allArgs.length > 0) {
440
+ firstPos !== null && firstPos !== void 0 ? firstPos : (firstPos = allArgs[0]);
441
+ startPos !== null && startPos !== void 0 ? startPos : (startPos = firstPos);
442
+ lastPos = allArgs[allArgs.length - 1];
404
443
  }
405
- isFirstCommand = false;
406
444
  }
407
- return new Path(firstPos !== null && firstPos !== void 0 ? firstPos : Vec2.zero, commands);
445
+ return new Path(startPos !== null && startPos !== void 0 ? startPos : Vec2.zero, commands);
408
446
  }
409
447
  }
@@ -34,6 +34,8 @@ export default class Rect2 {
34
34
  get maxDimension(): number;
35
35
  get topRight(): import("./Vec3").default;
36
36
  get bottomLeft(): import("./Vec3").default;
37
+ get width(): number;
38
+ get height(): number;
37
39
  getEdges(): LineSegment2[];
38
40
  transformedBoundingBox(affineTransform: Mat33): Rect2;
39
41
  /** @return true iff this is equal to [other] ± fuzz */
@@ -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
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].
@@ -115,6 +126,12 @@ export default class Rect2 {
115
126
  get bottomLeft() {
116
127
  return this.topLeft.plus(Vec2.of(0, this.h));
117
128
  }
129
+ get width() {
130
+ return this.w;
131
+ }
132
+ get height() {
133
+ return this.h;
134
+ }
118
135
  // Returns edges in the order
119
136
  // [ rightEdge, topEdge, leftEdge, bottomEdge ]
120
137
  getEdges() {
@@ -23,7 +23,7 @@ export default class Display {
23
23
  else {
24
24
  throw new Error(`Unknown rendering mode, ${mode}!`);
25
25
  }
26
- const cacheBlockResolution = Vec2.of(500, 500);
26
+ const cacheBlockResolution = Vec2.of(600, 600);
27
27
  this.cache = new RenderingCache({
28
28
  createRenderer: () => {
29
29
  if (mode === RenderingMode.DummyRenderer) {
@@ -45,8 +45,9 @@ export default class Display {
45
45
  },
46
46
  blockResolution: cacheBlockResolution,
47
47
  cacheSize: 500 * 500 * 4 * 200,
48
- maxScale: 1.4,
49
- minComponentsPerCache: 10,
48
+ maxScale: 1.5,
49
+ minComponentsPerCache: 50,
50
+ minComponentsToUseCache: 120,
50
51
  });
51
52
  this.editor.notifier.on(EditorEventType.DisplayResized, event => {
52
53
  var _a;
@@ -11,7 +11,7 @@ export default class CacheRecord {
11
11
  this.allocd = true;
12
12
  }
13
13
  startRender() {
14
- this.lastUsedCycle = this.cacheState.currentRenderingCycle++;
14
+ this.lastUsedCycle = this.cacheState.currentRenderingCycle;
15
15
  if (!this.allocd) {
16
16
  throw new Error('Only alloc\'d canvases can be rendered to');
17
17
  }
@@ -33,6 +33,7 @@ export default class CacheRecord {
33
33
  }
34
34
  this.allocd = true;
35
35
  this.onBeforeDeallocCallback = newDeallocCallback;
36
+ this.lastUsedCycle = this.cacheState.currentRenderingCycle;
36
37
  }
37
38
  getLastUsedCycle() {
38
39
  return this.lastUsedCycle;
@@ -25,15 +25,7 @@ export class CacheRecordManager {
25
25
  }
26
26
  // Returns null if there are no cache records. Returns an unalloc'd record if one exists.
27
27
  getLeastRecentlyUsedRecord() {
28
- let lruSoFar = null;
29
- for (const rec of this.cacheRecords) {
30
- if (!rec.isAllocd()) {
31
- return rec;
32
- }
33
- if (!lruSoFar || rec.getLastUsedCycle() < lruSoFar.getLastUsedCycle()) {
34
- lruSoFar = rec;
35
- }
36
- }
37
- return lruSoFar;
28
+ this.cacheRecords.sort((a, b) => a.getLastUsedCycle() - b.getLastUsedCycle());
29
+ return this.cacheRecords[0];
38
30
  }
39
31
  }
@@ -22,15 +22,21 @@ export default class RenderingCache {
22
22
  return;
23
23
  }
24
24
  if (!this.rootNode) {
25
- // Ensure that the node is just big enough to contain the entire viewport.
26
- const rootNodeSize = visibleRect.maxDimension;
25
+ // Adjust the node so that it has the correct aspect ratio
26
+ const res = this.partialSharedState.props.blockResolution;
27
27
  const topLeft = visibleRect.topLeft;
28
- this.rootNode = new RenderingCacheNode(new Rect2(topLeft.x, topLeft.y, rootNodeSize, rootNodeSize), this.getSharedState());
28
+ this.rootNode = new RenderingCacheNode(new Rect2(topLeft.x, topLeft.y, res.x, res.y), this.getSharedState());
29
29
  }
30
30
  while (!this.rootNode.region.containsRect(visibleRect)) {
31
31
  this.rootNode = this.rootNode.generateParent();
32
32
  }
33
33
  this.rootNode = (_a = this.rootNode.smallestChildContaining(visibleRect)) !== null && _a !== void 0 ? _a : this.rootNode;
34
- this.rootNode.renderItems(screenRenderer, [image], viewport);
34
+ const visibleLeaves = image.getLeavesIntersectingRegion(viewport.visibleRect, rect => screenRenderer.isTooSmallToRender(rect));
35
+ if (visibleLeaves.length > this.partialSharedState.props.minComponentsToUseCache) {
36
+ this.rootNode.renderItems(screenRenderer, [image], viewport);
37
+ }
38
+ else {
39
+ image.render(screenRenderer, visibleRect);
40
+ }
35
41
  }
36
42
  }
@@ -133,7 +133,11 @@ export default class RenderingCacheNode {
133
133
  const newItems = [];
134
134
  // Divide [items] until nodes are leaves or smaller than this
135
135
  for (const item of items) {
136
- if (item.getBBox().maxDimension >= this.region.maxDimension) {
136
+ const bbox = item.getBBox();
137
+ if (!bbox.intersects(this.region)) {
138
+ continue;
139
+ }
140
+ if (bbox.maxDimension >= this.region.maxDimension) {
137
141
  newItems.push(...item.getChildrenOrSelfIntersectingRegion(this.region));
138
142
  }
139
143
  else {
@@ -146,6 +150,9 @@ export default class RenderingCacheNode {
146
150
  items.forEach(item => item.render(screenRenderer, viewport.visibleRect));
147
151
  return;
148
152
  }
153
+ if (debugMode) {
154
+ screenRenderer.drawRect(this.region, 0.5 * viewport.getSizeOfPixelOnCanvas(), { fill: Color4.yellow });
155
+ }
149
156
  // Could we render direclty from [this] or do we need to recurse?
150
157
  const couldRender = this.renderingWouldBeHighEnoughResolution(viewport);
151
158
  if (!couldRender) {
@@ -159,7 +166,7 @@ export default class RenderingCacheNode {
159
166
  // Determine whether we already have rendered the items
160
167
  const leaves = [];
161
168
  for (const item of items) {
162
- leaves.push(...item.getLeavesIntersectingRegion(this.region));
169
+ leaves.push(...item.getLeavesIntersectingRegion(this.region, rect => rect.w / this.region.w < 2 / this.cacheState.props.blockResolution.x));
163
170
  }
164
171
  sortLeavesByZIndex(leaves);
165
172
  const leavesByIds = this.computeSortedByLeafIds(leaves);
@@ -213,7 +220,7 @@ export default class RenderingCacheNode {
213
220
  }
214
221
  }
215
222
  if (debugMode) {
216
- screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: Color4.yellow });
223
+ screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: Color4.clay });
217
224
  }
218
225
  }
219
226
  }
@@ -12,7 +12,7 @@ export const createCache = (onRenderAlloc, cacheOptions) => {
12
12
  },
13
13
  isOfCorrectType(renderer) {
14
14
  return renderer instanceof DummyRenderer;
15
- }, blockResolution: Vec2.of(500, 500), cacheSize: 500 * 10 * 4, maxScale: 2, minComponentsPerCache: 0 }, cacheOptions));
15
+ }, blockResolution: Vec2.of(500, 500), cacheSize: 500 * 10 * 4, maxScale: 2, minComponentsPerCache: 0, minComponentsToUseCache: 0 }, cacheOptions));
16
16
  return {
17
17
  cache,
18
18
  editor
@@ -10,6 +10,7 @@ export interface CacheProps {
10
10
  cacheSize: number;
11
11
  maxScale: number;
12
12
  minComponentsPerCache: number;
13
+ minComponentsToUseCache: number;
13
14
  }
14
15
  export interface PartialCacheState {
15
16
  currentRenderingCycle: number;
@@ -1,4 +1,6 @@
1
1
  import Color4 from '../../Color4';
2
+ import { LoadSaveDataTable } from '../../components/AbstractComponent';
3
+ import { TextStyle } from '../../components/Text';
2
4
  import Mat33 from '../../geometry/Mat33';
3
5
  import { PathCommand } from '../../geometry/Path';
4
6
  import Rect2 from '../../geometry/Rect2';
@@ -19,6 +21,7 @@ export interface RenderablePathSpec {
19
21
  export default abstract class AbstractRenderer {
20
22
  private viewport;
21
23
  private selfTransform;
24
+ private transformStack;
22
25
  protected constructor(viewport: Viewport);
23
26
  protected getViewport(): Viewport;
24
27
  abstract displaySize(): Vec2;
@@ -29,6 +32,7 @@ export default abstract class AbstractRenderer {
29
32
  protected abstract moveTo(point: Point2): void;
30
33
  protected abstract traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2): void;
31
34
  protected abstract traceQuadraticBezierCurve(controlPoint: Point2, endPoint: Point2): void;
35
+ abstract drawText(text: string, transform: Mat33, style: TextStyle): void;
32
36
  abstract isTooSmallToRender(rect: Rect2): boolean;
33
37
  setDraftMode(_draftMode: boolean): void;
34
38
  protected objectLevel: number;
@@ -37,12 +41,14 @@ export default abstract class AbstractRenderer {
37
41
  drawPath(path: RenderablePathSpec): void;
38
42
  drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle): void;
39
43
  startObject(_boundingBox: Rect2, _clip?: boolean): void;
40
- endObject(): void;
44
+ endObject(_loaderData?: LoadSaveDataTable): void;
41
45
  protected getNestingLevel(): number;
42
46
  abstract drawPoints(...points: Point2[]): void;
43
47
  canRenderFromWithoutDataLoss(_other: AbstractRenderer): boolean;
44
48
  renderFromOtherOfSameType(_renderTo: Mat33, other: AbstractRenderer): void;
45
49
  setTransform(transform: Mat33 | null): void;
50
+ pushTransform(transform: Mat33): void;
51
+ popTransform(): void;
46
52
  getCanvasToScreenTransform(): Mat33;
47
53
  canvasToScreen(vec: Vec2): Vec2;
48
54
  getSizeOfCanvasPixelOnScreen(): number;
@@ -11,6 +11,7 @@ export default class AbstractRenderer {
11
11
  this.viewport = viewport;
12
12
  // If null, this' transformation is linked to the Viewport
13
13
  this.selfTransform = null;
14
+ this.transformStack = [];
14
15
  this.objectLevel = 0;
15
16
  this.currentPaths = null;
16
17
  }
@@ -79,7 +80,7 @@ export default class AbstractRenderer {
79
80
  this.currentPaths = [];
80
81
  this.objectLevel++;
81
82
  }
82
- endObject() {
83
+ endObject(_loaderData) {
83
84
  // Render the paths all at once
84
85
  this.flushPath();
85
86
  this.currentPaths = null;
@@ -104,6 +105,17 @@ export default class AbstractRenderer {
104
105
  setTransform(transform) {
105
106
  this.selfTransform = transform;
106
107
  }
108
+ pushTransform(transform) {
109
+ this.transformStack.push(this.selfTransform);
110
+ this.setTransform(this.getCanvasToScreenTransform().rightMul(transform));
111
+ }
112
+ popTransform() {
113
+ var _a;
114
+ if (this.transformStack.length === 0) {
115
+ throw new Error('Unable to pop more transforms than have been pushed!');
116
+ }
117
+ this.setTransform((_a = this.transformStack.pop()) !== null && _a !== void 0 ? _a : null);
118
+ }
107
119
  // Get the matrix that transforms a vector on the canvas to a vector on this'
108
120
  // rendering target.
109
121
  getCanvasToScreenTransform() {
@@ -1,3 +1,4 @@
1
+ import { TextStyle } from '../../components/Text';
1
2
  import Mat33 from '../../geometry/Mat33';
2
3
  import Rect2 from '../../geometry/Rect2';
3
4
  import { Point2, Vec2 } from '../../geometry/Vec2';
@@ -12,6 +13,7 @@ export default class CanvasRenderer extends AbstractRenderer {
12
13
  private minRenderSizeAnyDimen;
13
14
  private minRenderSizeBothDimens;
14
15
  constructor(ctx: CanvasRenderingContext2D, viewport: Viewport);
16
+ private transformBy;
15
17
  canRenderFromWithoutDataLoss(other: AbstractRenderer): boolean;
16
18
  renderFromOtherOfSameType(transformBy: Mat33, other: AbstractRenderer): void;
17
19
  setDraftMode(draftMode: boolean): void;
@@ -24,6 +26,7 @@ export default class CanvasRenderer extends AbstractRenderer {
24
26
  protected traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2): void;
25
27
  protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
26
28
  drawPath(path: RenderablePathSpec): void;
29
+ drawText(text: string, transform: Mat33, style: TextStyle): void;
27
30
  private clipLevels;
28
31
  startObject(boundingBox: Rect2, clip: boolean): void;
29
32
  endObject(): void;
@@ -1,4 +1,5 @@
1
1
  import Color4 from '../../Color4';
2
+ import Text from '../../components/Text';
2
3
  import { Vec2 } from '../../geometry/Vec2';
3
4
  import AbstractRenderer from './AbstractRenderer';
4
5
  export default class CanvasRenderer extends AbstractRenderer {
@@ -10,6 +11,16 @@ export default class CanvasRenderer extends AbstractRenderer {
10
11
  this.clipLevels = [];
11
12
  this.setDraftMode(false);
12
13
  }
14
+ transformBy(transformBy) {
15
+ // From MDN, transform(a,b,c,d,e,f)
16
+ // takes input such that
17
+ // ⎡ a c e ⎤
18
+ // ⎢ b d f ⎥ transforms content drawn to [ctx].
19
+ // ⎣ 0 0 1 ⎦
20
+ this.ctx.transform(transformBy.a1, transformBy.b1, // a, b
21
+ transformBy.a2, transformBy.b2, // c, d
22
+ transformBy.a3, transformBy.b3);
23
+ }
13
24
  canRenderFromWithoutDataLoss(other) {
14
25
  return other instanceof CanvasRenderer;
15
26
  }
@@ -19,14 +30,7 @@ export default class CanvasRenderer extends AbstractRenderer {
19
30
  }
20
31
  transformBy = this.getCanvasToScreenTransform().rightMul(transformBy);
21
32
  this.ctx.save();
22
- // From MDN, transform(a,b,c,d,e,f)
23
- // takes input such that
24
- // ⎡ a c e ⎤
25
- // ⎢ b d f ⎥ transforms content drawn to [ctx].
26
- // ⎣ 0 0 1 ⎦
27
- this.ctx.transform(transformBy.a1, transformBy.b1, // a, b
28
- transformBy.a2, transformBy.b2, // c, d
29
- transformBy.a3, transformBy.b3);
33
+ this.transformBy(transformBy);
30
34
  this.ctx.drawImage(other.ctx.canvas, 0, 0);
31
35
  this.ctx.restore();
32
36
  }
@@ -105,6 +109,22 @@ export default class CanvasRenderer extends AbstractRenderer {
105
109
  }
106
110
  super.drawPath(path);
107
111
  }
112
+ drawText(text, transform, style) {
113
+ this.ctx.save();
114
+ transform = this.getCanvasToScreenTransform().rightMul(transform);
115
+ this.transformBy(transform);
116
+ Text.applyTextStyles(this.ctx, style);
117
+ if (style.renderingStyle.fill.a !== 0) {
118
+ this.ctx.fillStyle = style.renderingStyle.fill.toHexString();
119
+ this.ctx.fillText(text, 0, 0);
120
+ }
121
+ if (style.renderingStyle.stroke) {
122
+ this.ctx.strokeStyle = style.renderingStyle.stroke.color.toHexString();
123
+ this.ctx.lineWidth = style.renderingStyle.stroke.width;
124
+ this.ctx.strokeText(text, 0, 0);
125
+ }
126
+ this.ctx.restore();
127
+ }
108
128
  startObject(boundingBox, clip) {
109
129
  if (this.isTooSmallToRender(boundingBox)) {
110
130
  this.ignoreObjectsAboveLevel = this.getNestingLevel();
@@ -1,3 +1,4 @@
1
+ import { TextStyle } from '../../components/Text';
1
2
  import Mat33 from '../../geometry/Mat33';
2
3
  import Rect2 from '../../geometry/Rect2';
3
4
  import { Point2, Vec2 } from '../../geometry/Vec2';
@@ -10,6 +11,7 @@ export default class DummyRenderer extends AbstractRenderer {
10
11
  lastFillStyle: RenderingStyle | null;
11
12
  lastPoint: Point2 | null;
12
13
  objectNestingLevel: number;
14
+ lastText: string | null;
13
15
  pointBuffer: Point2[];
14
16
  constructor(viewport: Viewport);
15
17
  displaySize(): Vec2;
@@ -21,6 +23,7 @@ export default class DummyRenderer extends AbstractRenderer {
21
23
  protected traceCubicBezierCurve(p1: Vec3, p2: Vec3, p3: Vec3): void;
22
24
  protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
23
25
  drawPoints(..._points: Vec3[]): void;
26
+ drawText(text: string, _transform: Mat33, _style: TextStyle): void;
24
27
  startObject(boundingBox: Rect2, _clip: boolean): void;
25
28
  endObject(): void;
26
29
  isTooSmallToRender(_rect: Rect2): boolean;
@@ -10,6 +10,7 @@ export default class DummyRenderer extends AbstractRenderer {
10
10
  this.lastFillStyle = null;
11
11
  this.lastPoint = null;
12
12
  this.objectNestingLevel = 0;
13
+ this.lastText = null;
13
14
  // List of points drawn since the last clear.
14
15
  this.pointBuffer = [];
15
16
  }
@@ -28,6 +29,7 @@ export default class DummyRenderer extends AbstractRenderer {
28
29
  this.clearedCount++;
29
30
  this.renderedPathCount = 0;
30
31
  this.pointBuffer = [];
32
+ this.lastText = null;
31
33
  // Ensure all objects finished rendering
32
34
  if (this.objectNestingLevel > 0) {
33
35
  throw new Error(`Within an object while clearing! Nesting level: ${this.objectNestingLevel}`);
@@ -68,6 +70,9 @@ export default class DummyRenderer extends AbstractRenderer {
68
70
  // drawPoints is intended for debugging.
69
71
  // As such, it is unlikely to be the target of automated tests.
70
72
  }
73
+ drawText(text, _transform, _style) {
74
+ this.lastText = text;
75
+ }
71
76
  startObject(boundingBox, _clip) {
72
77
  super.startObject(boundingBox);
73
78
  this.objectNestingLevel += 1;