js-draw 0.10.1 → 0.10.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 (38) hide show
  1. package/CHANGELOG.md +3 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/components/AbstractComponent.d.ts +1 -0
  4. package/dist/src/components/AbstractComponent.js +6 -0
  5. package/dist/src/components/ImageComponent.d.ts +1 -0
  6. package/dist/src/components/ImageComponent.js +4 -0
  7. package/dist/src/components/Stroke.d.ts +2 -0
  8. package/dist/src/components/Stroke.js +5 -0
  9. package/dist/src/components/TextComponent.d.ts +1 -0
  10. package/dist/src/components/TextComponent.js +3 -0
  11. package/dist/src/components/util/StrokeSmoother.js +9 -5
  12. package/dist/src/math/Mat33.d.ts +2 -0
  13. package/dist/src/math/Mat33.js +7 -0
  14. package/dist/src/math/Path.js +3 -0
  15. package/dist/src/rendering/Display.js +5 -2
  16. package/dist/src/rendering/caching/RenderingCache.js +5 -1
  17. package/dist/src/rendering/caching/RenderingCacheNode.js +5 -2
  18. package/dist/src/rendering/caching/testUtils.js +1 -1
  19. package/dist/src/rendering/caching/types.d.ts +2 -2
  20. package/dist/src/toolbar/IconProvider.d.ts +1 -0
  21. package/dist/src/toolbar/IconProvider.js +12 -0
  22. package/dist/src/toolbar/localization.d.ts +1 -0
  23. package/dist/src/toolbar/localization.js +1 -0
  24. package/package.json +1 -1
  25. package/src/components/AbstractComponent.ts +8 -0
  26. package/src/components/ImageComponent.ts +5 -0
  27. package/src/components/Stroke.ts +11 -0
  28. package/src/components/TextComponent.ts +4 -0
  29. package/src/components/util/StrokeSmoother.ts +12 -9
  30. package/src/math/Mat33.ts +9 -0
  31. package/src/math/Path.ts +4 -0
  32. package/src/rendering/Display.ts +7 -2
  33. package/src/rendering/caching/RenderingCache.ts +10 -2
  34. package/src/rendering/caching/RenderingCacheNode.ts +6 -2
  35. package/src/rendering/caching/testUtils.ts +2 -2
  36. package/src/rendering/caching/types.ts +2 -2
  37. package/src/toolbar/IconProvider.ts +13 -0
  38. package/src/toolbar/localization.ts +3 -0
@@ -29,6 +29,7 @@ export default abstract class AbstractComponent {
29
29
  protected abstract applyTransformation(affineTransfm: Mat33): void;
30
30
  transformBy(affineTransfm: Mat33): SerializableCommand;
31
31
  isSelectable(): boolean;
32
+ getProportionalRenderingTime(): number;
32
33
  private static transformElementCommandId;
33
34
  private static UnresolvedTransformElementCommand;
34
35
  private static TransformElementCommand;
@@ -52,6 +52,12 @@ export default class AbstractComponent {
52
52
  isSelectable() {
53
53
  return true;
54
54
  }
55
+ // @returns an approximation of the proportional time it takes to render this component.
56
+ // This is intended to be a rough estimate, but, for example, a stroke with two points sould have
57
+ // a renderingWeight approximately twice that of a stroke with one point.
58
+ getProportionalRenderingTime() {
59
+ return 1;
60
+ }
55
61
  // Returns a copy of this component.
56
62
  clone() {
57
63
  const clone = this.createClone();
@@ -12,6 +12,7 @@ export default class ImageComponent extends AbstractComponent {
12
12
  private recomputeBBox;
13
13
  static fromImage(elem: HTMLImageElement, transform: Mat33): Promise<ImageComponent>;
14
14
  render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
15
+ getProportionalRenderingTime(): number;
15
16
  intersects(lineSegment: LineSegment2): boolean;
16
17
  protected serializeToJSON(): {
17
18
  src: string;
@@ -80,6 +80,10 @@ export default class ImageComponent extends AbstractComponent {
80
80
  render(canvas, _visibleRect) {
81
81
  canvas.drawImage(this.image);
82
82
  }
83
+ getProportionalRenderingTime() {
84
+ // Estimate: Equivalent to a stroke with 10 segments.
85
+ return 10;
86
+ }
83
87
  intersects(lineSegment) {
84
88
  const rect = this.getImageRect();
85
89
  const edges = rect.getEdges().map(edge => edge.transformedBy(this.image.transform));
@@ -8,9 +8,11 @@ import { ImageComponentLocalization } from './localization';
8
8
  export default class Stroke extends AbstractComponent {
9
9
  private parts;
10
10
  protected contentBBox: Rect2;
11
+ private approximateRenderingTime;
11
12
  constructor(parts: RenderablePathSpec[]);
12
13
  intersects(line: LineSegment2): boolean;
13
14
  render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
15
+ getProportionalRenderingTime(): number;
14
16
  private bboxForPart;
15
17
  protected applyTransformation(affineTransfm: Mat33): void;
16
18
  getPath(): Path;
@@ -7,6 +7,7 @@ export default class Stroke extends AbstractComponent {
7
7
  constructor(parts) {
8
8
  var _a;
9
9
  super('stroke');
10
+ this.approximateRenderingTime = 0;
10
11
  this.parts = [];
11
12
  for (const section of parts) {
12
13
  const path = Path.fromRenderable(section);
@@ -24,6 +25,7 @@ export default class Stroke extends AbstractComponent {
24
25
  style: section.style,
25
26
  commands: path.parts,
26
27
  });
28
+ this.approximateRenderingTime += path.parts.length;
27
29
  }
28
30
  (_a = this.contentBBox) !== null && _a !== void 0 ? _a : (this.contentBBox = Rect2.empty);
29
31
  }
@@ -53,6 +55,9 @@ export default class Stroke extends AbstractComponent {
53
55
  }
54
56
  canvas.endObject(this.getLoadSaveData());
55
57
  }
58
+ getProportionalRenderingTime() {
59
+ return this.approximateRenderingTime;
60
+ }
56
61
  // Grows the bounding box for a given stroke part based on that part's style.
57
62
  bboxForPart(origBBox, style) {
58
63
  if (!style.stroke) {
@@ -26,6 +26,7 @@ export default class TextComponent extends AbstractComponent {
26
26
  private recomputeBBox;
27
27
  private renderInternal;
28
28
  render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
29
+ getProportionalRenderingTime(): number;
29
30
  intersects(lineSegment: LineSegment2): boolean;
30
31
  getBaselinePos(): import("../lib").Vec3;
31
32
  getTextStyle(): TextStyle;
@@ -91,6 +91,9 @@ export default class TextComponent extends AbstractComponent {
91
91
  this.renderInternal(canvas);
92
92
  canvas.endObject(this.getLoadSaveData());
93
93
  }
94
+ getProportionalRenderingTime() {
95
+ return this.textObjects.length;
96
+ }
94
97
  intersects(lineSegment) {
95
98
  // Convert canvas space to internal space.
96
99
  const invTransform = this.transform.inverse();
@@ -60,6 +60,7 @@ export class StrokeSmoother {
60
60
  this.buffer[this.buffer.length - 2], lastPoint,
61
61
  ];
62
62
  this.currentCurve = null;
63
+ this.isFirstSegment = false;
63
64
  }
64
65
  // Returns [upper curve, connector, lower curve]
65
66
  currentSegmentToPath() {
@@ -116,20 +117,23 @@ export class StrokeSmoother {
116
117
  const pointRadius = newPoint.width;
117
118
  const prevEndWidth = this.curveEndWidth;
118
119
  this.curveEndWidth = pointRadius;
119
- if (this.isFirstSegment) {
120
- // The start of a curve often lacks accurate pressure information. Update it.
121
- this.curveStartWidth = (this.curveStartWidth + pointRadius) / 2;
122
- }
123
120
  // recompute bbox
124
121
  this.bbox = this.bbox.grownToPoint(newPoint.pos, pointRadius);
122
+ // If the last curve just ended or it's the first curve,
125
123
  if (this.currentCurve === null) {
126
124
  const p1 = lastPoint.pos;
127
125
  const p2 = lastPoint.pos.plus((_b = this.lastExitingVec) !== null && _b !== void 0 ? _b : Vec2.unitX);
128
126
  const p3 = newPoint.pos;
129
127
  // Quadratic Bézier curve
130
128
  this.currentCurve = new Bezier(p1.xy, p2.xy, p3.xy);
131
- this.curveStartWidth = lastPoint.width;
132
129
  console.assert(!isNaN(p1.magnitude()) && !isNaN(p2.magnitude()) && !isNaN(p3.magnitude()), 'Expected !NaN');
130
+ if (this.isFirstSegment) {
131
+ // The start of a curve often lacks accurate pressure information. Update it.
132
+ this.curveStartWidth = (this.curveStartWidth + pointRadius) / 2;
133
+ }
134
+ else {
135
+ this.curveStartWidth = prevEndWidth;
136
+ }
133
137
  }
134
138
  // If there isn't an entering vector (e.g. because this.isFirstCurve), approximate it.
135
139
  let enteringVec = this.lastExitingVec;
@@ -69,6 +69,8 @@ export default class Mat33 {
69
69
  * This is the standard way of transforming vectors in ℝ³.
70
70
  */
71
71
  transformVec3(other: Vec3): Vec3;
72
+ /** @returns true iff this is the identity matrix. */
73
+ isIdentity(): boolean;
72
74
  /** Returns true iff this = other ± fuzz */
73
75
  eq(other: Mat33, fuzz?: number): boolean;
74
76
  toString(): string;
@@ -153,6 +153,13 @@ export default class Mat33 {
153
153
  transformVec3(other) {
154
154
  return Vec3.of(this.rows[0].dot(other), this.rows[1].dot(other), this.rows[2].dot(other));
155
155
  }
156
+ /** @returns true iff this is the identity matrix. */
157
+ isIdentity() {
158
+ if (this === Mat33.identity) {
159
+ return true;
160
+ }
161
+ return this.eq(Mat33.identity);
162
+ }
156
163
  /** Returns true iff this = other ± fuzz */
157
164
  eq(other, fuzz = 0) {
158
165
  for (let i = 0; i < 3; i++) {
@@ -179,6 +179,9 @@ export default class Path {
179
179
  return new Path(startPoint, newParts);
180
180
  }
181
181
  transformedBy(affineTransfm) {
182
+ if (affineTransfm.isIdentity()) {
183
+ return this;
184
+ }
182
185
  return this.mapPoints(point => affineTransfm.transformVec2(point));
183
186
  }
184
187
  // Creates a new path by joining [other] to the end of this path
@@ -73,8 +73,11 @@ export default class Display {
73
73
  blockResolution: cacheBlockResolution,
74
74
  cacheSize: 600 * 600 * 4 * 90,
75
75
  maxScale: 1.4,
76
- minComponentsPerCache: 20,
77
- minComponentsToUseCache: 105,
76
+ // Require about 20 strokes with 4 parts each to cache an image in one of the
77
+ // parts of the cache grid.
78
+ minProportionalRenderTimePerCache: 20 * 4,
79
+ // Require about 105 strokes with 4 parts each to use the cache at all.
80
+ minProportionalRenderTimeToUseCache: 105 * 4,
78
81
  });
79
82
  this.editor.notifier.on(EditorEventType.DisplayResized, event => {
80
83
  var _a;
@@ -31,7 +31,11 @@ export default class RenderingCache {
31
31
  }
32
32
  this.rootNode = (_a = this.rootNode.smallestChildContaining(visibleRect)) !== null && _a !== void 0 ? _a : this.rootNode;
33
33
  const visibleLeaves = image.getLeavesIntersectingRegion(viewport.visibleRect, rect => screenRenderer.isTooSmallToRender(rect));
34
- if (visibleLeaves.length > this.sharedState.props.minComponentsToUseCache) {
34
+ let approxVisibleRenderTime = 0;
35
+ for (const leaf of visibleLeaves) {
36
+ approxVisibleRenderTime += leaf.getContent().getProportionalRenderingTime();
37
+ }
38
+ if (approxVisibleRenderTime > this.sharedState.props.minProportionalRenderTimeToUseCache) {
35
39
  this.rootNode.renderItems(screenRenderer, [image], viewport);
36
40
  }
37
41
  else {
@@ -197,9 +197,12 @@ export default class RenderingCacheNode {
197
197
  }
198
198
  return;
199
199
  }
200
+ let leafApproxRenderTime = 0;
201
+ for (const leaf of leavesByIds) {
202
+ leafApproxRenderTime += leaf.getContent().getProportionalRenderingTime();
203
+ }
200
204
  // Is it worth it to render the items?
201
- // TODO: Consider replacing this with something performace based.
202
- if (leavesByIds.length > this.cacheState.props.minComponentsPerCache) {
205
+ if (leafApproxRenderTime > this.cacheState.props.minProportionalRenderTimePerCache) {
203
206
  let fullRerenderNeeded = true;
204
207
  if (!this.cachedRenderer) {
205
208
  this.cachedRenderer = this.cacheState.recordManager.allocCanvas(this.region, () => this.onRegionDealloc());
@@ -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, minComponentsToUseCache: 0 }, cacheOptions));
15
+ }, blockResolution: Vec2.of(500, 500), cacheSize: 500 * 10 * 4, maxScale: 2, minProportionalRenderTimePerCache: 0, minProportionalRenderTimeToUseCache: 0 }, cacheOptions));
16
16
  return {
17
17
  cache,
18
18
  editor
@@ -9,8 +9,8 @@ export interface CacheProps {
9
9
  blockResolution: Vec2;
10
10
  cacheSize: number;
11
11
  maxScale: number;
12
- minComponentsPerCache: number;
13
- minComponentsToUseCache: number;
12
+ minProportionalRenderTimePerCache: number;
13
+ minProportionalRenderTimeToUseCache: number;
14
14
  }
15
15
  export interface CacheState {
16
16
  currentRenderingCycle: number;
@@ -25,6 +25,7 @@ export default class IconProvider {
25
25
  makePipetteIcon(color?: Color4): IconType;
26
26
  makeResizeViewportIcon(): IconType;
27
27
  makeDuplicateSelectionIcon(): IconType;
28
+ makePasteIcon(): IconType;
28
29
  makeDeleteSelectionIcon(): IconType;
29
30
  makeSaveIcon(): IconType;
30
31
  }
@@ -433,6 +433,18 @@ export default class IconProvider {
433
433
  M 10,25 10,90 70,90 70,60 40,60 40,25 10,25 z
434
434
  `);
435
435
  }
436
+ makePasteIcon() {
437
+ const icon = this.makeIconFromPath(`
438
+ M 50 0 L 50 5 L 35 5 L 40 24.75 L 20 25 L 20 100 L 85 100 L 100 90 L 100 24 L 75.1 24.3 L 80 5 L 65 5 L 65 0 L 50 0 z
439
+ M 10 15 L 10 115 L 110 115 L 110 15 L 85 15 L 83 20 L 105 20 L 105 110 L 15 110 L 15 20 L 32 20 L 30 15 L 10 15 z
440
+ M 25 35 L 90 35 L 90 40 L 25 40 L 25 35 z
441
+ M 25 45 L 90 45 L 90 50 L 25 50 L 25 45 z
442
+ M 25 55 L 85 55 L 85 60 L 25 60 L 25 55 z
443
+ M 25 65 L 90 65 L 90 70 L 25 70 L 25 65 z
444
+ `);
445
+ icon.setAttribute('viewBox', '0 0 120 120');
446
+ return icon;
447
+ }
436
448
  makeDeleteSelectionIcon() {
437
449
  const strokeWidth = '5px';
438
450
  const strokeColor = 'var(--icon-color)';
@@ -26,6 +26,7 @@ export interface ToolbarLocalization {
26
26
  zoom: string;
27
27
  resetView: string;
28
28
  selectionToolKeyboardShortcuts: string;
29
+ paste: string;
29
30
  dropdownShown: (toolName: string) => string;
30
31
  dropdownHidden: (toolName: string) => string;
31
32
  zoomLevel: (zoomPercentage: number) => string;
@@ -26,6 +26,7 @@ export const defaultToolbarLocalization = {
26
26
  outlinedRectanglePen: 'Outlined rectangle',
27
27
  filledRectanglePen: 'Filled rectangle',
28
28
  lockRotation: 'Lock rotation',
29
+ paste: 'Paste',
29
30
  dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
30
31
  dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
31
32
  zoomLevel: (zoomPercent) => `Zoom: ${zoomPercent}%`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "main": "./dist/src/lib.d.ts",
6
6
  "types": "./dist/src/lib.js",
@@ -70,6 +70,7 @@ export default abstract class AbstractComponent {
70
70
  public getZIndex(): number {
71
71
  return this.zIndex;
72
72
  }
73
+
73
74
  public getBBox(): Rect2 {
74
75
  return this.contentBBox;
75
76
  }
@@ -94,6 +95,13 @@ export default abstract class AbstractComponent {
94
95
  return true;
95
96
  }
96
97
 
98
+ // @returns an approximation of the proportional time it takes to render this component.
99
+ // This is intended to be a rough estimate, but, for example, a stroke with two points sould have
100
+ // a renderingWeight approximately twice that of a stroke with one point.
101
+ public getProportionalRenderingTime(): number {
102
+ return 1;
103
+ }
104
+
97
105
  private static transformElementCommandId = 'transform-element';
98
106
 
99
107
  private static UnresolvedTransformElementCommand = class extends SerializableCommand {
@@ -88,6 +88,11 @@ export default class ImageComponent extends AbstractComponent {
88
88
  canvas.drawImage(this.image);
89
89
  }
90
90
 
91
+ public getProportionalRenderingTime(): number {
92
+ // Estimate: Equivalent to a stroke with 10 segments.
93
+ return 10;
94
+ }
95
+
91
96
  public intersects(lineSegment: LineSegment2): boolean {
92
97
  const rect = this.getImageRect();
93
98
  const edges = rect.getEdges().map(edge => edge.transformedBy(this.image.transform));
@@ -15,11 +15,16 @@ export default class Stroke extends AbstractComponent {
15
15
  private parts: StrokePart[];
16
16
  protected contentBBox: Rect2;
17
17
 
18
+ // See `getProportionalRenderingTime`
19
+ private approximateRenderingTime: number;
20
+
18
21
  // Creates a `Stroke` from the given `parts`.
19
22
  public constructor(parts: RenderablePathSpec[]) {
20
23
  super('stroke');
21
24
 
25
+ this.approximateRenderingTime = 0;
22
26
  this.parts = [];
27
+
23
28
  for (const section of parts) {
24
29
  const path = Path.fromRenderable(section);
25
30
  const pathBBox = this.bboxForPart(path.bbox, section.style);
@@ -38,6 +43,8 @@ export default class Stroke extends AbstractComponent {
38
43
  style: section.style,
39
44
  commands: path.parts,
40
45
  });
46
+
47
+ this.approximateRenderingTime += path.parts.length;
41
48
  }
42
49
  this.contentBBox ??= Rect2.empty;
43
50
  }
@@ -71,6 +78,10 @@ export default class Stroke extends AbstractComponent {
71
78
  canvas.endObject(this.getLoadSaveData());
72
79
  }
73
80
 
81
+ public getProportionalRenderingTime(): number {
82
+ return this.approximateRenderingTime;
83
+ }
84
+
74
85
  // Grows the bounding box for a given stroke part based on that part's style.
75
86
  private bboxForPart(origBBox: Rect2, style: RenderingStyle) {
76
87
  if (!style.stroke) {
@@ -121,6 +121,10 @@ export default class TextComponent extends AbstractComponent {
121
121
  canvas.endObject(this.getLoadSaveData());
122
122
  }
123
123
 
124
+ public getProportionalRenderingTime(): number {
125
+ return this.textObjects.length;
126
+ }
127
+
124
128
  public intersects(lineSegment: LineSegment2): boolean {
125
129
 
126
130
  // Convert canvas space to internal space.
@@ -98,6 +98,8 @@ export class StrokeSmoother {
98
98
  this.buffer[this.buffer.length - 2], lastPoint,
99
99
  ];
100
100
  this.currentCurve = null;
101
+
102
+ this.isFirstSegment = false;
101
103
  }
102
104
 
103
105
  // Returns [upper curve, connector, lower curve]
@@ -165,25 +167,26 @@ export class StrokeSmoother {
165
167
  const prevEndWidth = this.curveEndWidth;
166
168
  this.curveEndWidth = pointRadius;
167
169
 
168
- if (this.isFirstSegment) {
169
- // The start of a curve often lacks accurate pressure information. Update it.
170
- this.curveStartWidth = (this.curveStartWidth + pointRadius) / 2;
171
- }
172
-
173
170
  // recompute bbox
174
171
  this.bbox = this.bbox.grownToPoint(newPoint.pos, pointRadius);
175
172
 
173
+ // If the last curve just ended or it's the first curve,
176
174
  if (this.currentCurve === null) {
177
175
  const p1 = lastPoint.pos;
178
176
  const p2 = lastPoint.pos.plus(this.lastExitingVec ?? Vec2.unitX);
179
177
  const p3 = newPoint.pos;
180
178
 
181
179
  // Quadratic Bézier curve
182
- this.currentCurve = new Bezier(
183
- p1.xy, p2.xy, p3.xy
184
- );
185
- this.curveStartWidth = lastPoint.width;
180
+ this.currentCurve = new Bezier(p1.xy, p2.xy, p3.xy);
186
181
  console.assert(!isNaN(p1.magnitude()) && !isNaN(p2.magnitude()) && !isNaN(p3.magnitude()), 'Expected !NaN');
182
+
183
+ if (this.isFirstSegment) {
184
+ // The start of a curve often lacks accurate pressure information. Update it.
185
+ this.curveStartWidth = (this.curveStartWidth + pointRadius) / 2;
186
+ }
187
+ else {
188
+ this.curveStartWidth = prevEndWidth;
189
+ }
187
190
  }
188
191
 
189
192
  // If there isn't an entering vector (e.g. because this.isFirstCurve), approximate it.
package/src/math/Mat33.ts CHANGED
@@ -219,6 +219,15 @@ export default class Mat33 {
219
219
  );
220
220
  }
221
221
 
222
+ /** @returns true iff this is the identity matrix. */
223
+ public isIdentity(): boolean {
224
+ if (this === Mat33.identity) {
225
+ return true;
226
+ }
227
+
228
+ return this.eq(Mat33.identity);
229
+ }
230
+
222
231
  /** Returns true iff this = other ± fuzz */
223
232
  public eq(other: Mat33, fuzz: number = 0): boolean {
224
233
  for (let i = 0; i < 3; i++) {
package/src/math/Path.ts CHANGED
@@ -248,6 +248,10 @@ export default class Path {
248
248
  }
249
249
 
250
250
  public transformedBy(affineTransfm: Mat33): Path {
251
+ if (affineTransfm.isIdentity()) {
252
+ return this;
253
+ }
254
+
251
255
  return this.mapPoints(point => affineTransfm.transformVec2(point));
252
256
  }
253
257
 
@@ -78,8 +78,13 @@ export default class Display {
78
78
  blockResolution: cacheBlockResolution,
79
79
  cacheSize: 600 * 600 * 4 * 90,
80
80
  maxScale: 1.4,
81
- minComponentsPerCache: 20,
82
- minComponentsToUseCache: 105,
81
+
82
+ // Require about 20 strokes with 4 parts each to cache an image in one of the
83
+ // parts of the cache grid.
84
+ minProportionalRenderTimePerCache: 20 * 4,
85
+
86
+ // Require about 105 strokes with 4 parts each to use the cache at all.
87
+ minProportionalRenderTimeToUseCache: 105 * 4,
83
88
  });
84
89
 
85
90
  this.editor.notifier.on(EditorEventType.DisplayResized, event => {
@@ -48,8 +48,16 @@ export default class RenderingCache {
48
48
 
49
49
  this.rootNode = this.rootNode!.smallestChildContaining(visibleRect) ?? this.rootNode;
50
50
 
51
- const visibleLeaves = image.getLeavesIntersectingRegion(viewport.visibleRect, rect => screenRenderer.isTooSmallToRender(rect));
52
- if (visibleLeaves.length > this.sharedState.props.minComponentsToUseCache) {
51
+ const visibleLeaves = image.getLeavesIntersectingRegion(
52
+ viewport.visibleRect, rect => screenRenderer.isTooSmallToRender(rect)
53
+ );
54
+
55
+ let approxVisibleRenderTime = 0;
56
+ for (const leaf of visibleLeaves) {
57
+ approxVisibleRenderTime += leaf.getContent()!.getProportionalRenderingTime();
58
+ }
59
+
60
+ if (approxVisibleRenderTime > this.sharedState.props.minProportionalRenderTimeToUseCache) {
53
61
  this.rootNode!.renderItems(screenRenderer, [ image ], viewport);
54
62
  } else {
55
63
  image.render(screenRenderer, visibleRect);
@@ -250,9 +250,13 @@ export default class RenderingCacheNode {
250
250
  return;
251
251
  }
252
252
 
253
+ let leafApproxRenderTime = 0;
254
+ for (const leaf of leavesByIds) {
255
+ leafApproxRenderTime += leaf.getContent()!.getProportionalRenderingTime();
256
+ }
257
+
253
258
  // Is it worth it to render the items?
254
- // TODO: Consider replacing this with something performace based.
255
- if (leavesByIds.length > this.cacheState.props.minComponentsPerCache) {
259
+ if (leafApproxRenderTime > this.cacheState.props.minProportionalRenderTimePerCache) {
256
260
  let fullRerenderNeeded = true;
257
261
  if (!this.cachedRenderer) {
258
262
  this.cachedRenderer = this.cacheState.recordManager.allocCanvas(
@@ -23,8 +23,8 @@ export const createCache = (onRenderAlloc?: RenderAllocCallback, cacheOptions?:
23
23
  blockResolution: Vec2.of(500, 500),
24
24
  cacheSize: 500 * 10 * 4,
25
25
  maxScale: 2,
26
- minComponentsPerCache: 0,
27
- minComponentsToUseCache: 0,
26
+ minProportionalRenderTimePerCache: 0,
27
+ minProportionalRenderTimeToUseCache: 0,
28
28
  ...cacheOptions
29
29
  });
30
30
 
@@ -20,11 +20,11 @@ export interface CacheProps {
20
20
  maxScale: number;
21
21
 
22
22
  // Minimum component count to cache, rather than just re-render each time.
23
- minComponentsPerCache: number;
23
+ minProportionalRenderTimePerCache: number;
24
24
 
25
25
  // Minimum number of strokes/etc. to use the cache to render, isntead of
26
26
  // rendering directly.
27
- minComponentsToUseCache: number;
27
+ minProportionalRenderTimeToUseCache: number;
28
28
  }
29
29
 
30
30
  export interface CacheState {
@@ -502,6 +502,19 @@ export default class IconProvider {
502
502
  M 10,25 10,90 70,90 70,60 40,60 40,25 10,25 z
503
503
  `);
504
504
  }
505
+
506
+ public makePasteIcon(): IconType {
507
+ const icon = this.makeIconFromPath(`
508
+ M 50 0 L 50 5 L 35 5 L 40 24.75 L 20 25 L 20 100 L 85 100 L 100 90 L 100 24 L 75.1 24.3 L 80 5 L 65 5 L 65 0 L 50 0 z
509
+ M 10 15 L 10 115 L 110 115 L 110 15 L 85 15 L 83 20 L 105 20 L 105 110 L 15 110 L 15 20 L 32 20 L 30 15 L 10 15 z
510
+ M 25 35 L 90 35 L 90 40 L 25 40 L 25 35 z
511
+ M 25 45 L 90 45 L 90 50 L 25 50 L 25 45 z
512
+ M 25 55 L 85 55 L 85 60 L 25 60 L 25 55 z
513
+ M 25 65 L 90 65 L 90 70 L 25 70 L 25 65 z
514
+ `);
515
+ icon.setAttribute('viewBox', '0 0 120 120');
516
+ return icon;
517
+ }
505
518
 
506
519
  public makeDeleteSelectionIcon(): IconType {
507
520
  const strokeWidth = '5px';
@@ -28,6 +28,7 @@ export interface ToolbarLocalization {
28
28
  zoom: string;
29
29
  resetView: string;
30
30
  selectionToolKeyboardShortcuts: string;
31
+ paste: string;
31
32
 
32
33
  dropdownShown: (toolName: string)=> string;
33
34
  dropdownHidden: (toolName: string)=> string;
@@ -66,6 +67,8 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
66
67
  filledRectanglePen: 'Filled rectangle',
67
68
  lockRotation: 'Lock rotation',
68
69
 
70
+ paste: 'Paste',
71
+
69
72
  dropdownShown: (toolName) => `Dropdown for ${toolName} shown`,
70
73
  dropdownHidden: (toolName) => `Dropdown for ${toolName} hidden`,
71
74
  zoomLevel: (zoomPercent: number) => `Zoom: ${zoomPercent}%`,