js-draw 0.10.0 → 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 (67) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Editor.d.ts +2 -2
  4. package/dist/src/EditorImage.d.ts +1 -1
  5. package/dist/src/EventDispatcher.d.ts +1 -1
  6. package/dist/src/SVGLoader.d.ts +2 -2
  7. package/dist/src/UndoRedoHistory.d.ts +2 -2
  8. package/dist/src/Viewport.d.ts +1 -1
  9. package/dist/src/commands/SerializableCommand.d.ts +1 -1
  10. package/dist/src/components/AbstractComponent.d.ts +4 -3
  11. package/dist/src/components/AbstractComponent.js +6 -0
  12. package/dist/src/components/ImageComponent.d.ts +1 -0
  13. package/dist/src/components/ImageComponent.js +4 -0
  14. package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -1
  15. package/dist/src/components/Stroke.d.ts +2 -0
  16. package/dist/src/components/Stroke.js +5 -0
  17. package/dist/src/components/TextComponent.d.ts +1 -0
  18. package/dist/src/components/TextComponent.js +3 -0
  19. package/dist/src/components/builders/FreehandLineBuilder.js +4 -3
  20. package/dist/src/components/builders/PressureSensitiveFreehandLineBuilder.js +1 -1
  21. package/dist/src/components/builders/types.d.ts +1 -1
  22. package/dist/src/components/util/StrokeSmoother.d.ts +1 -1
  23. package/dist/src/components/util/StrokeSmoother.js +11 -7
  24. package/dist/src/math/Mat33.d.ts +3 -1
  25. package/dist/src/math/Mat33.js +7 -0
  26. package/dist/src/math/Path.d.ts +1 -1
  27. package/dist/src/math/Path.js +3 -0
  28. package/dist/src/math/Vec2.d.ts +2 -2
  29. package/dist/src/rendering/Display.js +5 -2
  30. package/dist/src/rendering/caching/RenderingCache.js +5 -1
  31. package/dist/src/rendering/caching/RenderingCacheNode.js +5 -2
  32. package/dist/src/rendering/caching/testUtils.d.ts +1 -1
  33. package/dist/src/rendering/caching/testUtils.js +1 -1
  34. package/dist/src/rendering/caching/types.d.ts +4 -4
  35. package/dist/src/toolbar/IconProvider.d.ts +3 -2
  36. package/dist/src/toolbar/IconProvider.js +23 -3
  37. package/dist/src/toolbar/localization.d.ts +1 -0
  38. package/dist/src/toolbar/localization.js +1 -0
  39. package/dist/src/toolbar/makeColorInput.d.ts +2 -2
  40. package/dist/src/toolbar/widgets/BaseWidget.d.ts +1 -1
  41. package/dist/src/toolbar/widgets/PenToolWidget.js +2 -1
  42. package/dist/src/tools/BaseTool.js +4 -4
  43. package/dist/src/tools/PanZoom.js +13 -3
  44. package/dist/src/tools/PipetteTool.d.ts +1 -1
  45. package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +3 -3
  46. package/dist/src/tools/ToolbarShortcutHandler.d.ts +1 -1
  47. package/dist/src/types.d.ts +8 -8
  48. package/package.json +1 -1
  49. package/src/components/AbstractComponent.ts +8 -0
  50. package/src/components/ImageComponent.ts +5 -0
  51. package/src/components/Stroke.ts +11 -0
  52. package/src/components/TextComponent.ts +4 -0
  53. package/src/components/builders/FreehandLineBuilder.ts +4 -3
  54. package/src/components/builders/PressureSensitiveFreehandLineBuilder.ts +1 -1
  55. package/src/components/util/StrokeSmoother.ts +14 -11
  56. package/src/math/Mat33.ts +9 -0
  57. package/src/math/Path.ts +4 -0
  58. package/src/rendering/Display.ts +7 -2
  59. package/src/rendering/caching/RenderingCache.ts +10 -2
  60. package/src/rendering/caching/RenderingCacheNode.ts +6 -2
  61. package/src/rendering/caching/testUtils.ts +2 -2
  62. package/src/rendering/caching/types.ts +2 -2
  63. package/src/toolbar/IconProvider.ts +28 -3
  64. package/src/toolbar/localization.ts +3 -0
  65. package/src/toolbar/widgets/PenToolWidget.ts +3 -1
  66. package/src/tools/PanZoom.ts +16 -3
  67. package/.firebase/hosting.ZG9jcw.cache +0 -338
@@ -29,8 +29,8 @@ import Pointer from './Pointer';
29
29
  import Rect2 from './math/Rect2';
30
30
  import { EditorLocalization } from './localization';
31
31
  import IconProvider from './toolbar/IconProvider';
32
- declare type HTMLPointerEventType = 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel';
33
- declare type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent) => boolean;
32
+ type HTMLPointerEventType = 'pointerdown' | 'pointermove' | 'pointerup' | 'pointercancel';
33
+ type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent) => boolean;
34
34
  export interface EditorSettings {
35
35
  /** Defaults to `RenderingMode.CanvasRenderer` */
36
36
  renderingMode: RenderingMode;
@@ -27,7 +27,7 @@ export default class EditorImage {
27
27
  static addElement(elem: AbstractComponent, applyByFlattening?: boolean): SerializableCommand;
28
28
  private static AddElementCommand;
29
29
  }
30
- declare type TooSmallToRenderCheck = (rect: Rect2) => boolean;
30
+ type TooSmallToRenderCheck = (rect: Rect2) => boolean;
31
31
  /** Part of the Editor's image. @internal */
32
32
  export declare class ImageNode {
33
33
  private parent;
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * @packageDocumentation
17
17
  */
18
- declare type CallbackHandler<EventType> = (data: EventType) => void;
18
+ type CallbackHandler<EventType> = (data: EventType) => void;
19
19
  export default class EventDispatcher<EventKeyType extends string | symbol | number, EventMessageType> {
20
20
  private listeners;
21
21
  constructor();
@@ -3,8 +3,8 @@ import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnP
3
3
  export declare const defaultSVGViewRect: Rect2;
4
4
  export declare const svgAttributesDataKey = "svgAttrs";
5
5
  export declare const svgStyleAttributesDataKey = "svgStyleAttrs";
6
- export declare type SVGLoaderUnknownAttribute = [string, string];
7
- export declare type SVGLoaderUnknownStyleAttribute = {
6
+ export type SVGLoaderUnknownAttribute = [string, string];
7
+ export type SVGLoaderUnknownStyleAttribute = {
8
8
  key: string;
9
9
  value: string;
10
10
  priority?: string;
@@ -1,7 +1,7 @@
1
1
  import Editor from './Editor';
2
2
  import Command from './commands/Command';
3
- declare type AnnounceRedoCallback = (command: Command) => void;
4
- declare type AnnounceUndoCallback = (command: Command) => void;
3
+ type AnnounceRedoCallback = (command: Command) => void;
4
+ type AnnounceUndoCallback = (command: Command) => void;
5
5
  declare class UndoRedoHistory {
6
6
  private readonly editor;
7
7
  private announceRedoCallback;
@@ -4,7 +4,7 @@ import Rect2 from './math/Rect2';
4
4
  import { Point2, Vec2 } from './math/Vec2';
5
5
  import { StrokeDataPoint } from './types';
6
6
  import { EditorNotifier } from './types';
7
- declare type PointDataType<T extends Point2 | StrokeDataPoint | number> = T extends Point2 ? Point2 : number;
7
+ type PointDataType<T extends Point2 | StrokeDataPoint | number> = T extends Point2 ? Point2 : number;
8
8
  export declare abstract class ViewportTransform extends Command {
9
9
  abstract readonly transform: Mat33;
10
10
  }
@@ -1,6 +1,6 @@
1
1
  import Editor from '../Editor';
2
2
  import Command from './Command';
3
- export declare type DeserializationCallback = (data: Record<string, any> | any[], editor: Editor) => SerializableCommand;
3
+ export type DeserializationCallback = (data: Record<string, any> | any[], editor: Editor) => SerializableCommand;
4
4
  export default abstract class SerializableCommand extends Command {
5
5
  private commandTypeId;
6
6
  constructor(commandTypeId: string);
@@ -4,9 +4,9 @@ import Mat33 from '../math/Mat33';
4
4
  import Rect2 from '../math/Rect2';
5
5
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
6
6
  import { ImageComponentLocalization } from './localization';
7
- export declare type LoadSaveData = (string[] | Record<symbol, string | number>);
8
- export declare type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
9
- export declare type DeserializeCallback = (data: string) => AbstractComponent;
7
+ export type LoadSaveData = (string[] | Record<symbol, string | number>);
8
+ export type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
9
+ export type DeserializeCallback = (data: string) => AbstractComponent;
10
10
  export default abstract class AbstractComponent {
11
11
  private readonly componentKind;
12
12
  protected lastChangedTime: number;
@@ -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));
@@ -4,7 +4,7 @@ import Rect2 from '../math/Rect2';
4
4
  import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
5
  import AbstractComponent from './AbstractComponent';
6
6
  import { ImageComponentLocalization } from './localization';
7
- declare type GlobalAttrsList = Array<[string, string | null]>;
7
+ type GlobalAttrsList = Array<[string, string | null]>;
8
8
  export default class SVGGlobalAttributesObject extends AbstractComponent {
9
9
  private readonly attrs;
10
10
  protected contentBBox: Rect2;
@@ -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();
@@ -33,7 +33,7 @@ export default class FreehandLineBuilder {
33
33
  fill: Color4.transparent,
34
34
  stroke: {
35
35
  color: this.startPoint.color,
36
- width: this.roundDistance(this.averageWidth / 2),
36
+ width: this.roundDistance(this.averageWidth),
37
37
  }
38
38
  };
39
39
  }
@@ -77,7 +77,7 @@ export default class FreehandLineBuilder {
77
77
  return this.previewStroke();
78
78
  }
79
79
  getMinFit() {
80
- let minFit = Math.min(this.minFitAllowed, this.averageWidth / 5);
80
+ let minFit = Math.min(this.minFitAllowed, this.averageWidth / 3);
81
81
  if (minFit < 1e-10) {
82
82
  minFit = this.minFitAllowed;
83
83
  }
@@ -98,7 +98,8 @@ export default class FreehandLineBuilder {
98
98
  if (!this.isFirstSegment) {
99
99
  return [];
100
100
  }
101
- const width = Viewport.roundPoint(this.startPoint.width / 9, Math.min(this.minFitAllowed, this.startPoint.width / 5));
101
+ // Make the circle small -- because of the stroke style, we'll be drawing a stroke around it.
102
+ const width = Viewport.roundPoint(this.averageWidth / 10, Math.min(this.minFitAllowed, this.averageWidth / 10));
102
103
  const center = this.roundPoint(this.startPoint.pos);
103
104
  // Start on the right, cycle clockwise:
104
105
  // |
@@ -209,7 +209,7 @@ export default class PressureSensitiveFreehandLineBuilder {
209
209
  if (!this.isFirstSegment) {
210
210
  return;
211
211
  }
212
- const width = Viewport.roundPoint(this.startPoint.width / 3.5, Math.min(this.minFitAllowed, this.startPoint.width / 4));
212
+ const width = Viewport.roundPoint(this.startPoint.width / 2.2, Math.min(this.minFitAllowed, this.startPoint.width / 4));
213
213
  const center = this.roundPoint(this.startPoint.pos);
214
214
  // Start on the right, cycle clockwise:
215
215
  // |
@@ -9,4 +9,4 @@ export interface ComponentBuilder {
9
9
  preview(renderer: AbstractRenderer): void;
10
10
  addPoint(point: StrokeDataPoint): void;
11
11
  }
12
- export declare type ComponentBuilderFactory = (startPoint: StrokeDataPoint, viewport: Viewport) => ComponentBuilder;
12
+ export type ComponentBuilderFactory = (startPoint: StrokeDataPoint, viewport: Viewport) => ComponentBuilder;
@@ -8,7 +8,7 @@ export interface Curve {
8
8
  endWidth: number;
9
9
  endPoint: Vec2;
10
10
  }
11
- declare type OnCurveAddedCallback = (curve: Curve | null) => void;
11
+ type OnCurveAddedCallback = (curve: Curve | null) => void;
12
12
  export declare class StrokeSmoother {
13
13
  private startPoint;
14
14
  private minFitAllowed;
@@ -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() {
@@ -113,23 +114,26 @@ export class StrokeSmoother {
113
114
  const lastPoint = (_a = this.lastPoint) !== null && _a !== void 0 ? _a : newPoint;
114
115
  this.lastPoint = newPoint;
115
116
  this.buffer.push(newPoint.pos);
116
- const pointRadius = newPoint.width / 2;
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 / 2;
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;
@@ -190,7 +194,7 @@ export class StrokeSmoother {
190
194
  }
191
195
  return true;
192
196
  };
193
- if (this.buffer.length > 3 && this.approxCurrentCurveLength() > this.curveStartWidth) {
197
+ if (this.buffer.length > 3 && this.approxCurrentCurveLength() > this.curveStartWidth / 2) {
194
198
  if (!curveMatchesPoints(this.currentCurve)) {
195
199
  // Use a curve that better fits the points
196
200
  this.currentCurve = prevCurve;
@@ -1,6 +1,6 @@
1
1
  import { Point2, Vec2 } from './Vec2';
2
2
  import Vec3 from './Vec3';
3
- export declare type Mat33Array = [
3
+ export type Mat33Array = [
4
4
  number,
5
5
  number,
6
6
  number,
@@ -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++) {
@@ -30,7 +30,7 @@ export interface MoveToPathCommand {
30
30
  kind: PathCommandType.MoveTo;
31
31
  point: Point2;
32
32
  }
33
- export declare type PathCommand = CubicBezierPathCommand | LinePathCommand | QuadraticBezierPathCommand | MoveToPathCommand;
33
+ export type PathCommand = CubicBezierPathCommand | LinePathCommand | QuadraticBezierPathCommand | MoveToPathCommand;
34
34
  interface IntersectionResult {
35
35
  curve: LineSegment2 | Bezier;
36
36
  parameterValue: number;
@@ -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
@@ -9,5 +9,5 @@ export declare namespace Vec2 {
9
9
  const unitY: Vec3;
10
10
  const zero: Vec3;
11
11
  }
12
- export declare type Point2 = Vec3;
13
- export declare type Vec2 = Vec3;
12
+ export type Point2 = Vec3;
13
+ export type Vec2 = Vec3;
@@ -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());
@@ -1,7 +1,7 @@
1
1
  import DummyRenderer from '../renderers/DummyRenderer';
2
2
  import RenderingCache from './RenderingCache';
3
3
  import { CacheProps } from './types';
4
- declare type RenderAllocCallback = (renderer: DummyRenderer) => void;
4
+ type RenderAllocCallback = (renderer: DummyRenderer) => void;
5
5
  export declare const createCache: (onRenderAlloc?: RenderAllocCallback, cacheOptions?: Partial<CacheProps>) => {
6
6
  cache: RenderingCache;
7
7
  editor: import("../../Editor").Editor;
@@ -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
@@ -1,16 +1,16 @@
1
1
  import { Vec2 } from '../../math/Vec2';
2
2
  import AbstractRenderer from '../renderers/AbstractRenderer';
3
3
  import { CacheRecordManager } from './CacheRecordManager';
4
- export declare type CacheAddress = number;
5
- export declare type BeforeDeallocCallback = () => void;
4
+ export type CacheAddress = number;
5
+ export type BeforeDeallocCallback = () => void;
6
6
  export interface CacheProps {
7
7
  createRenderer(): AbstractRenderer;
8
8
  isOfCorrectType(renderer: AbstractRenderer): boolean;
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;
@@ -2,7 +2,7 @@ import Color4 from '../Color4';
2
2
  import { ComponentBuilderFactory } from '../components/builders/types';
3
3
  import { TextStyle } from '../components/TextComponent';
4
4
  import Pen from '../tools/Pen';
5
- declare type IconType = SVGSVGElement | HTMLImageElement;
5
+ type IconType = SVGSVGElement | HTMLImageElement;
6
6
  export default class IconProvider {
7
7
  makeUndoIcon(): IconType;
8
8
  makeRedoIcon(mirror?: boolean): IconType;
@@ -20,11 +20,12 @@ export default class IconProvider {
20
20
  makeZoomIcon(): IconType;
21
21
  makeRotationLockIcon(): IconType;
22
22
  makeTextIcon(textStyle: TextStyle): IconType;
23
- makePenIcon(tipThickness: number, color: string | Color4): IconType;
23
+ makePenIcon(tipThickness: number, color: string | Color4, roundedTip?: boolean): IconType;
24
24
  makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory): IconType;
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
  }
@@ -302,7 +302,7 @@ export default class IconProvider {
302
302
  icon.appendChild(textNode);
303
303
  return icon;
304
304
  }
305
- makePenIcon(tipThickness, color) {
305
+ makePenIcon(tipThickness, color, roundedTip) {
306
306
  if (color instanceof Color4) {
307
307
  color = color.toHexString();
308
308
  }
@@ -310,8 +310,16 @@ export default class IconProvider {
310
310
  icon.setAttribute('viewBox', '0 0 100 100');
311
311
  const halfThickness = tipThickness / 2;
312
312
  // Draw a pen-like shape
313
- const primaryStrokeTipPath = `M14,63 L${50 - halfThickness},95 L${50 + halfThickness},90 L88,60 Z`;
314
- const backgroundStrokeTipPath = `M14,63 L${50 - halfThickness},85 L${50 + halfThickness},83 L88,60 Z`;
313
+ const penTipLeft = 50 - halfThickness;
314
+ const penTipRight = 50 + halfThickness;
315
+ let tipCenterPrimaryPath = `L${penTipLeft},95 L${penTipRight},90`;
316
+ let tipCenterBackgroundPath = `L${penTipLeft},85 L${penTipRight},83`;
317
+ if (roundedTip) {
318
+ tipCenterPrimaryPath = `L${penTipLeft},95 q${halfThickness},10 ${2 * halfThickness},-5`;
319
+ tipCenterBackgroundPath = `L${penTipLeft},87 q${halfThickness},10 ${2 * halfThickness},-3`;
320
+ }
321
+ const primaryStrokeTipPath = `M14,63 ${tipCenterPrimaryPath} L88,60 Z`;
322
+ const backgroundStrokeTipPath = `M14,63 ${tipCenterBackgroundPath} L88,60 Z`;
315
323
  icon.innerHTML = `
316
324
  <defs>
317
325
  ${checkerboardPatternDef}
@@ -425,6 +433,18 @@ export default class IconProvider {
425
433
  M 10,25 10,90 70,90 70,60 40,60 40,25 10,25 z
426
434
  `);
427
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
+ }
428
448
  makeDeleteSelectionIcon() {
429
449
  const strokeWidth = '5px';
430
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}%`,
@@ -1,6 +1,6 @@
1
1
  import Color4 from '../Color4';
2
2
  import Editor from '../Editor';
3
- declare type OnColorChangeListener = (color: Color4) => void;
4
- declare type SetColorCallback = (color: Color4 | string) => void;
3
+ type OnColorChangeListener = (color: Color4) => void;
4
+ type SetColorCallback = (color: Color4 | string) => void;
5
5
  export declare const makeColorInput: (editor: Editor, onColorChange: OnColorChangeListener) => [HTMLInputElement, HTMLElement, SetColorCallback];
6
6
  export default makeColorInput;
@@ -1,7 +1,7 @@
1
1
  import Editor from '../../Editor';
2
2
  import { KeyPressEvent } from '../../types';
3
3
  import { ToolbarLocalization } from '../localization';
4
- export declare type SavedToolbuttonState = Record<string, any>;
4
+ export type SavedToolbuttonState = Record<string, any>;
5
5
  export default abstract class BaseWidget {
6
6
  #private;
7
7
  protected editor: Editor;
@@ -87,7 +87,8 @@ export default class PenToolWidget extends BaseToolWidget {
87
87
  // Use a square-root scale to prevent the pen's tip from overflowing.
88
88
  const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
89
89
  const color = this.tool.getColor();
90
- return this.editor.icons.makePenIcon(scale, color.toHexString());
90
+ const roundedTip = strokeFactory === makeFreehandLineBuilder;
91
+ return this.editor.icons.makePenIcon(scale, color.toHexString(), roundedTip);
91
92
  }
92
93
  else {
93
94
  const strokeFactory = this.tool.getStrokeFactory();
@@ -1,15 +1,15 @@
1
1
  import { EditorEventType } from '../types';
2
2
  export default class BaseTool {
3
+ onPointerDown(_event) { return false; }
4
+ onPointerMove(_event) { }
5
+ onPointerUp(_event) { }
6
+ onGestureCancel() { }
3
7
  constructor(notifier, description) {
4
8
  this.notifier = notifier;
5
9
  this.description = description;
6
10
  this.enabled = true;
7
11
  this.group = null;
8
12
  }
9
- onPointerDown(_event) { return false; }
10
- onPointerMove(_event) { }
11
- onPointerUp(_event) { }
12
- onGestureCancel() { }
13
13
  onWheel(_event) {
14
14
  return false;
15
15
  }
@@ -115,13 +115,18 @@ export default class PanZoom extends BaseTool {
115
115
  updateVelocity(currentCenter) {
116
116
  const deltaPos = currentCenter.minus(this.lastScreenCenter);
117
117
  const deltaTime = ((new Date()).getTime() - this.lastTimestamp) / 1000;
118
- const currentVelocity = deltaPos.times(1 / deltaTime);
119
- let smoothedVelocity = currentVelocity;
118
+ // We divide by deltaTime. Don't divide by zero.
120
119
  if (deltaTime === 0) {
121
120
  return;
122
121
  }
122
+ // Ignore duplicate events, unless there has been enough time between them.
123
+ if (deltaPos.magnitude() === 0 && deltaTime < 0.1) {
124
+ return;
125
+ }
126
+ const currentVelocity = deltaPos.times(1 / deltaTime);
127
+ let smoothedVelocity = currentVelocity;
123
128
  if (this.velocity) {
124
- smoothedVelocity = this.velocity.lerp(smoothedVelocity, 0.5);
129
+ smoothedVelocity = this.velocity.lerp(currentVelocity, 0.5);
125
130
  }
126
131
  this.velocity = smoothedVelocity;
127
132
  }
@@ -181,6 +186,11 @@ export default class PanZoom extends BaseTool {
181
186
  };
182
187
  const shouldInertialScroll = event.current.device === PointerDevice.Touch && event.allPointers.length === 1;
183
188
  if (shouldInertialScroll && this.velocity !== null) {
189
+ // If the user drags the screen, then stops, then lifts the pointer,
190
+ // we want the final velocity to reflect the stop at the end (so the velocity
191
+ // should be near zero). Handle this:
192
+ this.updateVelocity(event.current.screenPos);
193
+ // Cancel any ongoing inertial scrolling.
184
194
  (_a = this.inertialScroller) === null || _a === void 0 ? void 0 : _a.stop();
185
195
  this.inertialScroller = new InertialScroller(this.velocity, (scrollDelta) => {
186
196
  if (!this.transform) {