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
@@ -6,8 +6,8 @@ import UndoRedoHistory from './UndoRedoHistory';
6
6
  import Viewport from './Viewport';
7
7
  import { Point2 } from './geometry/Vec2';
8
8
  import HTMLToolbar from './toolbar/HTMLToolbar';
9
- import { RenderablePathSpec } from './rendering/AbstractRenderer';
10
- import Display, { RenderingMode } from './Display';
9
+ import { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
10
+ import Display, { RenderingMode } from './rendering/Display';
11
11
  import Pointer from './Pointer';
12
12
  import Rect2 from './geometry/Rect2';
13
13
  import { EditorLocalization } from './localization';
@@ -16,8 +16,8 @@ import EventDispatcher from './EventDispatcher';
16
16
  import { Vec2 } from './geometry/Vec2';
17
17
  import Vec3 from './geometry/Vec3';
18
18
  import HTMLToolbar from './toolbar/HTMLToolbar';
19
- import Display, { RenderingMode } from './Display';
20
- import SVGRenderer from './rendering/SVGRenderer';
19
+ import Display, { RenderingMode } from './rendering/Display';
20
+ import SVGRenderer from './rendering/renderers/SVGRenderer';
21
21
  import Color4 from './Color4';
22
22
  import SVGLoader from './SVGLoader';
23
23
  import Pointer from './Pointer';
@@ -115,6 +115,10 @@ export class Editor {
115
115
  // May be required to prevent text selection on iOS/Safari:
116
116
  // See https://stackoverflow.com/a/70992717/17055750
117
117
  this.renderingRegion.addEventListener('touchstart', evt => evt.preventDefault());
118
+ this.renderingRegion.addEventListener('contextmenu', evt => {
119
+ // Don't show a context menu
120
+ evt.preventDefault();
121
+ });
118
122
  this.renderingRegion.addEventListener('pointerdown', evt => {
119
123
  const pointer = Pointer.ofEvent(evt, true, this.viewport);
120
124
  pointers[pointer.id] = pointer;
@@ -282,7 +286,8 @@ export class Editor {
282
286
  const exportRectStrokeWidth = 12;
283
287
  renderer.drawRect(this.importExportViewport.visibleRect, exportRectStrokeWidth, exportRectFill);
284
288
  }
285
- this.image.render(renderer, this.viewport);
289
+ //this.image.render(renderer, this.viewport);
290
+ this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
286
291
  this.rerenderQueued = false;
287
292
  }
288
293
  drawWetInk(...path) {
@@ -344,20 +349,25 @@ export class Editor {
344
349
  loadFrom(loader) {
345
350
  return __awaiter(this, void 0, void 0, function* () {
346
351
  this.showLoadingWarning(0);
347
- const imageRect = yield loader.start((component) => {
352
+ this.display.setDraftMode(true);
353
+ yield loader.start((component) => {
348
354
  (new EditorImage.AddElementCommand(component)).apply(this);
349
355
  }, (countProcessed, totalToProcess) => {
350
- if (countProcessed % 100 === 0) {
356
+ if (countProcessed % 500 === 0) {
351
357
  this.showLoadingWarning(countProcessed / totalToProcess);
352
- this.rerender(false);
358
+ this.rerender();
353
359
  return new Promise(resolve => {
354
360
  requestAnimationFrame(() => resolve());
355
361
  });
356
362
  }
357
363
  return null;
364
+ }, (importExportRect) => {
365
+ this.setImportExportRect(importExportRect).apply(this);
366
+ this.viewport.zoomTo(importExportRect).apply(this);
358
367
  });
359
368
  this.hideLoadingWarning();
360
- this.setImportExportRect(imageRect).apply(this);
369
+ this.display.setDraftMode(false);
370
+ this.queueRerender();
361
371
  });
362
372
  }
363
373
  // Returns the size of the visible region of the output SVG
@@ -1,16 +1,18 @@
1
1
  import Editor from './Editor';
2
- import AbstractRenderer from './rendering/AbstractRenderer';
2
+ import AbstractRenderer from './rendering/renderers/AbstractRenderer';
3
3
  import Viewport from './Viewport';
4
4
  import AbstractComponent from './components/AbstractComponent';
5
5
  import Rect2 from './geometry/Rect2';
6
6
  import { EditorLocalization } from './localization';
7
+ import RenderingCache from './rendering/caching/RenderingCache';
8
+ export declare const sortLeavesByZIndex: (leaves: Array<ImageNode>) => void;
7
9
  export default class EditorImage {
8
10
  private root;
9
11
  constructor();
10
12
  private addElement;
11
13
  findParent(elem: AbstractComponent): ImageNode | null;
12
- private sortLeaves;
13
- render(renderer: AbstractRenderer, viewport: Viewport, minFraction?: number): void;
14
+ renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport): void;
15
+ render(renderer: AbstractRenderer, viewport: Viewport): void;
14
16
  renderAll(renderer: AbstractRenderer): void;
15
17
  getElementsIntersectingRegion(region: Rect2): AbstractComponent[];
16
18
  static AddElementCommand: {
@@ -24,23 +26,29 @@ export default class EditorImage {
24
26
  };
25
27
  }
26
28
  export declare type AddElementCommand = typeof EditorImage.AddElementCommand.prototype;
29
+ declare type TooSmallToRenderCheck = (rect: Rect2) => boolean;
27
30
  export declare class ImageNode {
28
31
  private parent;
29
32
  private content;
30
33
  private bbox;
31
34
  private children;
32
35
  private targetChildCount;
33
- private minZIndex;
34
- private maxZIndex;
36
+ private id;
37
+ private static idCounter;
35
38
  constructor(parent?: ImageNode | null);
39
+ getId(): number;
40
+ onContentChange(): void;
36
41
  getContent(): AbstractComponent | null;
37
42
  getParent(): ImageNode | null;
38
- private getChildrenInRegion;
39
- getLeavesInRegion(region: Rect2, minFractionOfRegion?: number): ImageNode[];
43
+ private getChildrenIntersectingRegion;
44
+ getChildrenOrSelfIntersectingRegion(region: Rect2): ImageNode[];
45
+ getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[];
40
46
  getLeaves(): ImageNode[];
41
47
  addLeaf(leaf: AbstractComponent): ImageNode;
42
48
  getBBox(): Rect2;
43
49
  recomputeBBox(bubbleUp: boolean): void;
44
50
  private rebalance;
45
51
  remove(): void;
52
+ render(renderer: AbstractRenderer, visibleRect: Rect2): void;
46
53
  }
54
+ export {};
@@ -11,6 +11,9 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
11
11
  };
12
12
  var _element, _applyByFlattening, _a;
13
13
  import Rect2 from './geometry/Rect2';
14
+ export const sortLeavesByZIndex = (leaves) => {
15
+ leaves.sort((a, b) => a.getContent().getZIndex() - b.getContent().getZIndex());
16
+ };
14
17
  // Handles lookup/storage of elements in the image
15
18
  export default class EditorImage {
16
19
  constructor() {
@@ -21,7 +24,7 @@ export default class EditorImage {
21
24
  }
22
25
  // Returns the parent of the given element, if it exists.
23
26
  findParent(elem) {
24
- const candidates = this.root.getLeavesInRegion(elem.getBBox());
27
+ const candidates = this.root.getLeavesIntersectingRegion(elem.getBBox());
25
28
  for (const candidate of candidates) {
26
29
  if (candidate.getContent() === elem) {
27
30
  return candidate;
@@ -29,29 +32,23 @@ export default class EditorImage {
29
32
  }
30
33
  return null;
31
34
  }
32
- sortLeaves(leaves) {
33
- leaves.sort((a, b) => a.getContent().zIndex - b.getContent().zIndex);
35
+ renderWithCache(screenRenderer, cache, viewport) {
36
+ cache.render(screenRenderer, this.root, viewport);
34
37
  }
35
- render(renderer, viewport, minFraction = 0.001) {
36
- // Don't render components that are < 0.1% of the viewport.
37
- const leaves = this.root.getLeavesInRegion(viewport.visibleRect, minFraction);
38
- this.sortLeaves(leaves);
39
- for (const leaf of leaves) {
40
- // Leaves by definition have content
41
- leaf.getContent().render(renderer, viewport.visibleRect);
42
- }
38
+ render(renderer, viewport) {
39
+ this.root.render(renderer, viewport.visibleRect);
43
40
  }
44
41
  // Renders all nodes, even ones not within the viewport
45
42
  renderAll(renderer) {
46
43
  const leaves = this.root.getLeaves();
47
- this.sortLeaves(leaves);
44
+ sortLeavesByZIndex(leaves);
48
45
  for (const leaf of leaves) {
49
46
  leaf.getContent().render(renderer, leaf.getBBox());
50
47
  }
51
48
  }
52
49
  getElementsIntersectingRegion(region) {
53
- const leaves = this.root.getLeavesInRegion(region);
54
- this.sortLeaves(leaves);
50
+ const leaves = this.root.getLeavesIntersectingRegion(region);
51
+ sortLeavesByZIndex(leaves);
55
52
  return leaves.map(leaf => leaf.getContent());
56
53
  }
57
54
  }
@@ -65,6 +62,9 @@ EditorImage.AddElementCommand = (_a = class {
65
62
  _applyByFlattening.set(this, false);
66
63
  __classPrivateFieldSet(this, _element, element, "f");
67
64
  __classPrivateFieldSet(this, _applyByFlattening, applyByFlattening, "f");
65
+ if (isNaN(__classPrivateFieldGet(this, _element, "f").getBBox().area)) {
66
+ throw new Error('Elements in the image cannot have NaN bounding boxes');
67
+ }
68
68
  }
69
69
  apply(editor) {
70
70
  editor.image.addElement(__classPrivateFieldGet(this, _element, "f"));
@@ -88,6 +88,7 @@ EditorImage.AddElementCommand = (_a = class {
88
88
  _element = new WeakMap(),
89
89
  _applyByFlattening = new WeakMap(),
90
90
  _a);
91
+ // TODO: Assign leaf nodes to CacheNodes. When leaf nodes are modified, the corresponding CacheNodes can be updated.
91
92
  export class ImageNode {
92
93
  constructor(parent = null) {
93
94
  this.parent = parent;
@@ -95,8 +96,13 @@ export class ImageNode {
95
96
  this.children = [];
96
97
  this.bbox = Rect2.empty;
97
98
  this.content = null;
98
- this.minZIndex = null;
99
- this.maxZIndex = null;
99
+ this.id = ImageNode.idCounter++;
100
+ }
101
+ getId() {
102
+ return this.id;
103
+ }
104
+ onContentChange() {
105
+ this.id = ImageNode.idCounter++;
100
106
  }
101
107
  getContent() {
102
108
  return this.content;
@@ -104,24 +110,30 @@ export class ImageNode {
104
110
  getParent() {
105
111
  return this.parent;
106
112
  }
107
- getChildrenInRegion(region) {
113
+ getChildrenIntersectingRegion(region) {
108
114
  return this.children.filter(child => {
109
115
  return child.getBBox().intersects(region);
110
116
  });
111
117
  }
118
+ getChildrenOrSelfIntersectingRegion(region) {
119
+ if (this.content) {
120
+ return [this];
121
+ }
122
+ return this.getChildrenIntersectingRegion(region);
123
+ }
112
124
  // Returns a list of `ImageNode`s with content (and thus no children).
113
- getLeavesInRegion(region, minFractionOfRegion = 0) {
125
+ getLeavesIntersectingRegion(region, isTooSmall) {
114
126
  const result = [];
115
127
  // Don't render if too small
116
- if (this.bbox.maxDimension / region.maxDimension <= minFractionOfRegion) {
128
+ if (isTooSmall === null || isTooSmall === void 0 ? void 0 : isTooSmall(this.bbox)) {
117
129
  return [];
118
130
  }
119
131
  if (this.content !== null && this.getBBox().intersects(region)) {
120
132
  result.push(this);
121
133
  }
122
- const children = this.getChildrenInRegion(region);
134
+ const children = this.getChildrenIntersectingRegion(region);
123
135
  for (const child of children) {
124
- result.push(...child.getLeavesInRegion(region, minFractionOfRegion));
136
+ result.push(...child.getLeavesIntersectingRegion(region, isTooSmall));
125
137
  }
126
138
  return result;
127
139
  }
@@ -138,6 +150,7 @@ export class ImageNode {
138
150
  return result;
139
151
  }
140
152
  addLeaf(leaf) {
153
+ this.onContentChange();
141
154
  if (this.content === null && this.children.length === 0) {
142
155
  this.content = leaf;
143
156
  this.recomputeBBox(true);
@@ -186,17 +199,13 @@ export class ImageNode {
186
199
  // this' ancestors bounding boxes. This also re-computes this' bounding box
187
200
  // in the z-direction (z-indicies).
188
201
  recomputeBBox(bubbleUp) {
189
- var _a, _b, _c;
202
+ var _a;
190
203
  const oldBBox = this.bbox;
191
204
  if (this.content !== null) {
192
205
  this.bbox = this.content.getBBox();
193
- this.minZIndex = this.content.zIndex;
194
- this.maxZIndex = this.content.zIndex;
195
206
  }
196
207
  else {
197
208
  this.bbox = Rect2.empty;
198
- this.minZIndex = null;
199
- this.maxZIndex = null;
200
209
  let isFirst = true;
201
210
  for (const child of this.children) {
202
211
  if (isFirst) {
@@ -206,18 +215,10 @@ export class ImageNode {
206
215
  else {
207
216
  this.bbox = this.bbox.union(child.getBBox());
208
217
  }
209
- (_a = this.minZIndex) !== null && _a !== void 0 ? _a : (this.minZIndex = child.minZIndex);
210
- (_b = this.maxZIndex) !== null && _b !== void 0 ? _b : (this.maxZIndex = child.maxZIndex);
211
- if (child.minZIndex !== null && this.minZIndex !== null) {
212
- this.minZIndex = Math.min(child.minZIndex, this.minZIndex);
213
- }
214
- if (child.maxZIndex !== null && this.maxZIndex !== null) {
215
- this.maxZIndex = Math.max(child.maxZIndex, this.maxZIndex);
216
- }
217
218
  }
218
219
  }
219
220
  if (bubbleUp && !oldBBox.eq(this.bbox)) {
220
- (_c = this.parent) === null || _c === void 0 ? void 0 : _c.recomputeBBox(true);
221
+ (_a = this.parent) === null || _a === void 0 ? void 0 : _a.recomputeBBox(true);
221
222
  }
222
223
  }
223
224
  rebalance() {
@@ -243,8 +244,6 @@ export class ImageNode {
243
244
  }
244
245
  // Remove this node and all of its children
245
246
  remove() {
246
- this.minZIndex = null;
247
- this.maxZIndex = null;
248
247
  if (!this.parent) {
249
248
  this.content = null;
250
249
  this.children = [];
@@ -264,4 +263,14 @@ export class ImageNode {
264
263
  this.parent = null;
265
264
  this.children = [];
266
265
  }
266
+ render(renderer, visibleRect) {
267
+ // Don't render components that are < 0.1% of the viewport.
268
+ const leaves = this.getLeavesIntersectingRegion(visibleRect, rect => renderer.isTooSmallToRender(rect));
269
+ sortLeavesByZIndex(leaves);
270
+ for (const leaf of leaves) {
271
+ // Leaves by definition have content
272
+ leaf.getContent().render(renderer, visibleRect);
273
+ }
274
+ }
267
275
  }
276
+ ImageNode.idCounter = 0;
@@ -4,8 +4,9 @@ export declare enum PointerDevice {
4
4
  Pen = 0,
5
5
  Eraser = 1,
6
6
  Touch = 2,
7
- Mouse = 3,
8
- Other = 4
7
+ PrimaryButtonMouse = 3,
8
+ RightButtonMouse = 4,
9
+ Other = 5
9
10
  }
10
11
  export default class Pointer {
11
12
  readonly screenPos: Point2;
@@ -4,8 +4,9 @@ export var PointerDevice;
4
4
  PointerDevice[PointerDevice["Pen"] = 0] = "Pen";
5
5
  PointerDevice[PointerDevice["Eraser"] = 1] = "Eraser";
6
6
  PointerDevice[PointerDevice["Touch"] = 2] = "Touch";
7
- PointerDevice[PointerDevice["Mouse"] = 3] = "Mouse";
8
- PointerDevice[PointerDevice["Other"] = 4] = "Other";
7
+ PointerDevice[PointerDevice["PrimaryButtonMouse"] = 3] = "PrimaryButtonMouse";
8
+ PointerDevice[PointerDevice["RightButtonMouse"] = 4] = "RightButtonMouse";
9
+ PointerDevice[PointerDevice["Other"] = 5] = "Other";
9
10
  })(PointerDevice || (PointerDevice = {}));
10
11
  // Provides a snapshot containing information about a pointer. A Pointer
11
12
  // object is immutable --- it will not be updated when the pointer's information changes.
@@ -34,7 +35,7 @@ export default class Pointer {
34
35
  var _a, _b;
35
36
  const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
36
37
  const pointerTypeToDevice = {
37
- 'mouse': PointerDevice.Mouse,
38
+ 'mouse': PointerDevice.PrimaryButtonMouse,
38
39
  'pen': PointerDevice.Pen,
39
40
  'touch': PointerDevice.Touch,
40
41
  };
@@ -45,6 +46,14 @@ export default class Pointer {
45
46
  }
46
47
  const timeStamp = (new Date()).getTime();
47
48
  const canvasPos = viewport.roundPoint(viewport.screenToCanvas(screenPos));
49
+ if (device === PointerDevice.PrimaryButtonMouse) {
50
+ if (evt.buttons & 0x2) {
51
+ device = PointerDevice.RightButtonMouse;
52
+ }
53
+ else if (!(evt.buttons & 0x1)) {
54
+ device = PointerDevice.Other;
55
+ }
56
+ }
48
57
  return new Pointer(screenPos, canvasPos, (_b = evt.pressure) !== null && _b !== void 0 ? _b : null, evt.isPrimary, isDown, device, evt.pointerId, timeStamp);
49
58
  }
50
59
  // Create a new Pointer from a point on the canvas.
@@ -1,23 +1,27 @@
1
1
  import Rect2 from './geometry/Rect2';
2
- import { ComponentAddedListener, ImageLoader, OnProgressListener } from './types';
2
+ import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
3
3
  export declare const defaultSVGViewRect: Rect2;
4
+ export declare const svgAttributesDataKey = "svgAttrs";
5
+ export declare type SVGLoaderUnknownAttribute = [string, string];
4
6
  export default class SVGLoader implements ImageLoader {
5
7
  private source;
6
8
  private onFinish?;
7
9
  private onAddComponent;
8
10
  private onProgress;
11
+ private onDetermineExportRect;
9
12
  private processedCount;
10
13
  private totalToProcess;
11
14
  private rootViewBox;
12
15
  private constructor();
13
16
  private getStyle;
14
17
  private strokeDataFromElem;
18
+ private attachUnrecognisedAttrs;
15
19
  private addPath;
16
20
  private addUnknownNode;
17
21
  private updateViewBox;
18
22
  private updateSVGAttrs;
19
23
  private visit;
20
24
  private getSourceAttrs;
21
- start(onAddComponent: ComponentAddedListener, onProgress: OnProgressListener): Promise<Rect2>;
25
+ start(onAddComponent: ComponentAddedListener, onProgress: OnProgressListener, onDetermineExportRect?: OnDetermineExportRectListener | null): Promise<void>;
22
26
  static fromString(text: string): SVGLoader;
23
27
  }
@@ -15,12 +15,15 @@ import Path from './geometry/Path';
15
15
  import Rect2 from './geometry/Rect2';
16
16
  // Size of a loaded image if no size is specified.
17
17
  export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
18
+ // Key to retrieve unrecognised attributes from an AbstractComponent
19
+ export const svgAttributesDataKey = 'svgAttrs';
18
20
  export default class SVGLoader {
19
21
  constructor(source, onFinish) {
20
22
  this.source = source;
21
23
  this.onFinish = onFinish;
22
24
  this.onAddComponent = null;
23
25
  this.onProgress = null;
26
+ this.onDetermineExportRect = null;
24
27
  this.processedCount = 0;
25
28
  this.totalToProcess = 0;
26
29
  }
@@ -80,6 +83,14 @@ export default class SVGLoader {
80
83
  }
81
84
  return result;
82
85
  }
86
+ attachUnrecognisedAttrs(elem, node, supportedAttrs) {
87
+ for (const attr of node.getAttributeNames()) {
88
+ if (supportedAttrs.has(attr)) {
89
+ continue;
90
+ }
91
+ elem.attachLoadSaveData(svgAttributesDataKey, [attr, node.getAttribute(attr)]);
92
+ }
93
+ }
83
94
  // Adds a stroke with a single path
84
95
  addPath(node) {
85
96
  var _a;
@@ -87,6 +98,7 @@ export default class SVGLoader {
87
98
  try {
88
99
  const strokeData = this.strokeDataFromElem(node);
89
100
  elem = new Stroke(strokeData);
101
+ this.attachUnrecognisedAttrs(elem, node, new Set(['stroke', 'fill', 'stroke-width', 'd']));
90
102
  }
91
103
  catch (e) {
92
104
  console.error('Invalid path in node', node, '\nError:', e, '\nAdding as an unknown object.');
@@ -100,6 +112,7 @@ export default class SVGLoader {
100
112
  (_a = this.onAddComponent) === null || _a === void 0 ? void 0 : _a.call(this, component);
101
113
  }
102
114
  updateViewBox(node) {
115
+ var _a;
103
116
  const viewBoxAttr = node.getAttribute('viewBox');
104
117
  if (this.rootViewBox || !viewBoxAttr) {
105
118
  return;
@@ -113,6 +126,7 @@ export default class SVGLoader {
113
126
  return;
114
127
  }
115
128
  this.rootViewBox = new Rect2(x, y, width, height);
129
+ (_a = this.onDetermineExportRect) === null || _a === void 0 ? void 0 : _a.call(this, this.rootViewBox);
116
130
  }
117
131
  updateSVGAttrs(node) {
118
132
  var _a;
@@ -154,23 +168,22 @@ export default class SVGLoader {
154
168
  return [attr, node.getAttribute(attr)];
155
169
  });
156
170
  }
157
- start(onAddComponent, onProgress) {
158
- var _a;
171
+ start(onAddComponent, onProgress, onDetermineExportRect = null) {
172
+ var _a, _b;
159
173
  return __awaiter(this, void 0, void 0, function* () {
160
174
  this.onAddComponent = onAddComponent;
161
175
  this.onProgress = onProgress;
176
+ this.onDetermineExportRect = onDetermineExportRect;
162
177
  // Estimate the number of tags to process.
163
178
  this.totalToProcess = this.source.childElementCount;
164
179
  this.processedCount = 0;
165
180
  this.rootViewBox = null;
166
181
  yield this.visit(this.source);
167
182
  const viewBox = this.rootViewBox;
168
- let result = defaultSVGViewRect;
169
- if (viewBox) {
170
- result = Rect2.of(viewBox);
183
+ if (!viewBox) {
184
+ (_a = this.onDetermineExportRect) === null || _a === void 0 ? void 0 : _a.call(this, defaultSVGViewRect);
171
185
  }
172
- (_a = this.onFinish) === null || _a === void 0 ? void 0 : _a.call(this);
173
- return result;
186
+ (_b = this.onFinish) === null || _b === void 0 ? void 0 : _b.call(this);
174
187
  });
175
188
  }
176
189
  // TODO: Handling unsafe data! Tripple-check that this is secure!
@@ -187,7 +200,6 @@ export default class SVGLoader {
187
200
  sandbox.remove();
188
201
  throw new Error('SVG loading iframe is not sandboxed.');
189
202
  }
190
- // Try running JavaScript within the iframe
191
203
  const sandboxDoc = (_b = (_a = sandbox.contentWindow) === null || _a === void 0 ? void 0 : _a.document) !== null && _b !== void 0 ? _b : sandbox.contentDocument;
192
204
  if (sandboxDoc == null)
193
205
  throw new Error('Unable to open a sandboxed iframe!');
@@ -1,3 +1,4 @@
1
+ import Command from './commands/Command';
1
2
  import { CommandLocalization } from './commands/localization';
2
3
  import Editor from './Editor';
3
4
  import Mat33 from './geometry/Mat33';
@@ -28,10 +29,13 @@ export declare class Viewport {
28
29
  resetTransform(newTransform: Mat33): void;
29
30
  get screenToCanvasTransform(): Mat33;
30
31
  get canvasToScreenTransform(): Mat33;
32
+ getResolution(): Vec2;
31
33
  getScaleFactor(): number;
34
+ getSizeOfPixelOnCanvas(): number;
32
35
  getRotationAngle(): number;
33
36
  static roundPoint<T extends Point2 | number>(point: T, tolerance: number): PointDataType<T>;
34
37
  roundPoint(point: Point2): Point2;
38
+ zoomTo(toMakeVisible: Rect2): Command;
35
39
  }
36
40
  export declare namespace Viewport {
37
41
  type ViewportTransform = typeof Viewport.ViewportTransform.prototype;
@@ -50,11 +50,17 @@ export class Viewport {
50
50
  get canvasToScreenTransform() {
51
51
  return this.transform;
52
52
  }
53
+ getResolution() {
54
+ return this.screenRect.size;
55
+ }
53
56
  // Returns the amount a vector on the canvas is scaled to become a vector on the screen.
54
57
  getScaleFactor() {
55
58
  // Use transformVec3 to avoid translating the vector
56
59
  return this.transform.transformVec3(Vec3.unitX).magnitude();
57
60
  }
61
+ getSizeOfPixelOnCanvas() {
62
+ return 1 / this.getScaleFactor();
63
+ }
58
64
  // Returns the angle of the canvas in radians
59
65
  getRotationAngle() {
60
66
  return this.transform.transformVec3(Vec3.unitX).angle();
@@ -76,6 +82,51 @@ export class Viewport {
76
82
  roundPoint(point) {
77
83
  return Viewport.roundPoint(point, 1 / this.getScaleFactor());
78
84
  }
85
+ // Returns a Command that transforms the view such that [rect] is visible, and perhaps
86
+ // centered in the viewport.
87
+ // Returns null if no transformation is necessary
88
+ zoomTo(toMakeVisible) {
89
+ let transform = Mat33.identity;
90
+ if (toMakeVisible.w === 0 || toMakeVisible.h === 0) {
91
+ throw new Error(`${toMakeVisible.toString()} rectangle is empty! Cannot zoom to!`);
92
+ }
93
+ if (isNaN(toMakeVisible.size.magnitude())) {
94
+ throw new Error(`${toMakeVisible.toString()} rectangle has NaN size! Cannot zoom to!`);
95
+ }
96
+ // Try to move the selection within the center 2/3rds of the viewport.
97
+ const recomputeTargetRect = () => {
98
+ // transform transforms objects on the canvas. As such, we need to invert it
99
+ // to transform the viewport.
100
+ const visibleRect = this.visibleRect.transformedBoundingBox(transform.inverse());
101
+ return visibleRect.transformedBoundingBox(Mat33.scaling2D(2 / 3, visibleRect.center));
102
+ };
103
+ let targetRect = recomputeTargetRect();
104
+ const largerThanTarget = targetRect.w < toMakeVisible.w || targetRect.h < toMakeVisible.h;
105
+ // Ensure that toMakeVisible is at least 1/8th of the visible region.
106
+ const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
107
+ if (largerThanTarget || muchSmallerThanTarget) {
108
+ // If larger than the target, ensure that the longest axis is visible.
109
+ // If smaller, shrink the visible rectangle as much as possible
110
+ const multiplier = (largerThanTarget ? Math.max : Math.min)(toMakeVisible.w / targetRect.w, toMakeVisible.h / targetRect.h);
111
+ const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
112
+ const viewportContentTransform = visibleRectTransform.inverse();
113
+ transform = transform.rightMul(viewportContentTransform);
114
+ }
115
+ targetRect = recomputeTargetRect();
116
+ // Ensure that the center of the region is visible
117
+ if (!targetRect.containsRect(toMakeVisible)) {
118
+ // target position - current position
119
+ const translation = toMakeVisible.center.minus(targetRect.center);
120
+ const visibleRectTransform = Mat33.translation(translation);
121
+ const viewportContentTransform = visibleRectTransform.inverse();
122
+ transform = transform.rightMul(viewportContentTransform);
123
+ }
124
+ if (!transform.invertable()) {
125
+ console.warn('Unable to zoom to ', toMakeVisible, '! Computed transform', transform, 'is singular.');
126
+ transform = Mat33.identity;
127
+ }
128
+ return new Viewport.ViewportTransform(transform);
129
+ }
79
130
  }
80
131
  // Command that translates/scales the viewport.
81
132
  Viewport.ViewportTransform = (_a = class {
@@ -2,14 +2,20 @@ import Command from '../commands/Command';
2
2
  import LineSegment2 from '../geometry/LineSegment2';
3
3
  import Mat33 from '../geometry/Mat33';
4
4
  import Rect2 from '../geometry/Rect2';
5
- import AbstractRenderer from '../rendering/AbstractRenderer';
5
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
6
6
  import { ImageComponentLocalization } from './localization';
7
+ declare type LoadSaveData = unknown;
8
+ export declare type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
7
9
  export default abstract class AbstractComponent {
8
10
  protected lastChangedTime: number;
9
11
  protected abstract contentBBox: Rect2;
10
- zIndex: number;
12
+ private zIndex;
11
13
  private static zIndexCounter;
12
14
  protected constructor();
15
+ private loadSaveData;
16
+ attachLoadSaveData(key: string, data: LoadSaveData): void;
17
+ getLoadSaveData(): LoadSaveDataTable;
18
+ getZIndex(): number;
13
19
  getBBox(): Rect2;
14
20
  abstract render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
15
21
  abstract intersects(lineSegment: LineSegment2): boolean;
@@ -17,3 +23,4 @@ export default abstract class AbstractComponent {
17
23
  transformBy(affineTransfm: Mat33): Command;
18
24
  abstract description(localizationTable: ImageComponentLocalization): string;
19
25
  }
26
+ export {};
@@ -1,9 +1,23 @@
1
1
  import EditorImage from '../EditorImage';
2
2
  export default class AbstractComponent {
3
3
  constructor() {
4
+ // Get and manage data attached by a loader.
5
+ this.loadSaveData = {};
4
6
  this.lastChangedTime = (new Date()).getTime();
5
7
  this.zIndex = AbstractComponent.zIndexCounter++;
6
8
  }
9
+ attachLoadSaveData(key, data) {
10
+ if (!this.loadSaveData[key]) {
11
+ this.loadSaveData[key] = [];
12
+ }
13
+ this.loadSaveData[key].push(data);
14
+ }
15
+ getLoadSaveData() {
16
+ return this.loadSaveData;
17
+ }
18
+ getZIndex() {
19
+ return this.zIndex;
20
+ }
7
21
  getBBox() {
8
22
  return this.contentBBox;
9
23
  }
@@ -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 SVGGlobalAttributesObject 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
  // Stores global SVG attributes (e.g. namespace identifiers.)
5
5
  export default class SVGGlobalAttributesObject extends AbstractComponent {
@@ -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, { RenderablePathSpec } from '../rendering/AbstractRenderer';
4
+ import AbstractRenderer, { RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
5
5
  import AbstractComponent from './AbstractComponent';
6
6
  import { ImageComponentLocalization } from './localization';
7
7
  export default class Stroke extends AbstractComponent {