js-draw 0.0.9 → 0.1.1

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 (104) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Editor.d.ts +2 -2
  4. package/dist/src/Editor.js +15 -7
  5. package/dist/src/EditorImage.d.ts +15 -7
  6. package/dist/src/EditorImage.js +43 -37
  7. package/dist/src/SVGLoader.d.ts +3 -2
  8. package/dist/src/SVGLoader.js +9 -7
  9. package/dist/src/Viewport.d.ts +4 -0
  10. package/dist/src/Viewport.js +41 -0
  11. package/dist/src/components/AbstractComponent.d.ts +3 -2
  12. package/dist/src/components/AbstractComponent.js +3 -0
  13. package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -1
  14. package/dist/src/components/SVGGlobalAttributesObject.js +1 -1
  15. package/dist/src/components/Stroke.d.ts +1 -1
  16. package/dist/src/components/UnknownSVGObject.d.ts +1 -1
  17. package/dist/src/components/UnknownSVGObject.js +1 -1
  18. package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
  19. package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -1
  20. package/dist/src/components/builders/FreehandLineBuilder.js +1 -1
  21. package/dist/src/components/builders/LineBuilder.d.ts +1 -1
  22. package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
  23. package/dist/src/components/builders/types.d.ts +1 -1
  24. package/dist/src/geometry/Mat33.js +3 -0
  25. package/dist/src/geometry/Path.d.ts +1 -1
  26. package/dist/src/geometry/Path.js +5 -3
  27. package/dist/src/geometry/Rect2.d.ts +1 -0
  28. package/dist/src/geometry/Rect2.js +47 -9
  29. package/dist/src/{Display.d.ts → rendering/Display.d.ts} +6 -2
  30. package/dist/src/{Display.js → rendering/Display.js} +37 -4
  31. package/dist/src/rendering/caching/CacheRecord.d.ts +19 -0
  32. package/dist/src/rendering/caching/CacheRecord.js +52 -0
  33. package/dist/src/rendering/caching/CacheRecordManager.d.ts +11 -0
  34. package/dist/src/rendering/caching/CacheRecordManager.js +31 -0
  35. package/dist/src/rendering/caching/RenderingCache.d.ts +12 -0
  36. package/dist/src/rendering/caching/RenderingCache.js +42 -0
  37. package/dist/src/rendering/caching/RenderingCacheNode.d.ts +28 -0
  38. package/dist/src/rendering/caching/RenderingCacheNode.js +301 -0
  39. package/dist/src/rendering/caching/testUtils.d.ts +9 -0
  40. package/dist/src/rendering/caching/testUtils.js +20 -0
  41. package/dist/src/rendering/caching/types.d.ts +21 -0
  42. package/dist/src/rendering/caching/types.js +1 -0
  43. package/dist/src/rendering/{AbstractRenderer.d.ts → renderers/AbstractRenderer.d.ts} +19 -8
  44. package/dist/src/rendering/{AbstractRenderer.js → renderers/AbstractRenderer.js} +37 -2
  45. package/dist/src/rendering/{CanvasRenderer.d.ts → renderers/CanvasRenderer.d.ts} +14 -5
  46. package/dist/src/rendering/renderers/CanvasRenderer.js +164 -0
  47. package/dist/src/rendering/{DummyRenderer.d.ts → renderers/DummyRenderer.d.ts} +9 -5
  48. package/dist/src/rendering/{DummyRenderer.js → renderers/DummyRenderer.js} +35 -4
  49. package/dist/src/rendering/{SVGRenderer.d.ts → renderers/SVGRenderer.d.ts} +4 -3
  50. package/dist/src/rendering/{SVGRenderer.js → renderers/SVGRenderer.js} +14 -11
  51. package/dist/src/testing/createEditor.js +1 -1
  52. package/dist/src/toolbar/HTMLToolbar.js +11 -2
  53. package/dist/src/toolbar/localization.d.ts +1 -0
  54. package/dist/src/toolbar/localization.js +1 -0
  55. package/dist/src/tools/PanZoom.js +3 -0
  56. package/dist/src/tools/SelectionTool.d.ts +3 -0
  57. package/dist/src/tools/SelectionTool.js +22 -24
  58. package/dist/src/types.d.ts +2 -1
  59. package/package.json +1 -1
  60. package/src/Editor.ts +17 -8
  61. package/src/EditorImage.test.ts +2 -2
  62. package/src/EditorImage.ts +54 -42
  63. package/src/SVGLoader.ts +11 -8
  64. package/src/Viewport.ts +56 -0
  65. package/src/components/AbstractComponent.ts +6 -2
  66. package/src/components/SVGGlobalAttributesObject.ts +2 -2
  67. package/src/components/Stroke.ts +1 -1
  68. package/src/components/UnknownSVGObject.ts +2 -2
  69. package/src/components/builders/ArrowBuilder.ts +1 -1
  70. package/src/components/builders/FreehandLineBuilder.ts +2 -2
  71. package/src/components/builders/LineBuilder.ts +1 -1
  72. package/src/components/builders/RectangleBuilder.ts +1 -1
  73. package/src/components/builders/types.ts +1 -1
  74. package/src/geometry/Mat33.ts +3 -0
  75. package/src/geometry/Path.toString.test.ts +12 -2
  76. package/src/geometry/Path.ts +8 -4
  77. package/src/geometry/Rect2.test.ts +47 -8
  78. package/src/geometry/Rect2.ts +57 -9
  79. package/src/{Display.ts → rendering/Display.ts} +43 -6
  80. package/src/rendering/caching/CacheRecord.test.ts +49 -0
  81. package/src/rendering/caching/CacheRecord.ts +73 -0
  82. package/src/rendering/caching/CacheRecordManager.ts +45 -0
  83. package/src/rendering/caching/RenderingCache.test.ts +44 -0
  84. package/src/rendering/caching/RenderingCache.ts +63 -0
  85. package/src/rendering/caching/RenderingCacheNode.ts +378 -0
  86. package/src/rendering/caching/testUtils.ts +35 -0
  87. package/src/rendering/caching/types.ts +39 -0
  88. package/src/rendering/{AbstractRenderer.ts → renderers/AbstractRenderer.ts} +57 -8
  89. package/src/rendering/renderers/CanvasRenderer.ts +219 -0
  90. package/src/rendering/renderers/DummyRenderer.test.ts +43 -0
  91. package/src/rendering/{DummyRenderer.ts → renderers/DummyRenderer.ts} +50 -7
  92. package/src/rendering/{SVGRenderer.ts → renderers/SVGRenderer.ts} +17 -13
  93. package/src/testing/createEditor.ts +1 -1
  94. package/src/toolbar/HTMLToolbar.ts +13 -2
  95. package/src/toolbar/localization.ts +2 -0
  96. package/src/tools/PanZoom.ts +3 -0
  97. package/src/tools/SelectionTool.test.ts +1 -1
  98. package/src/tools/SelectionTool.ts +28 -33
  99. package/src/types.ts +10 -3
  100. package/tsconfig.json +1 -0
  101. package/dist/__mocks__/coloris.d.ts +0 -2
  102. package/dist/__mocks__/coloris.js +0 -5
  103. package/dist/src/rendering/CanvasRenderer.js +0 -108
  104. package/src/rendering/CanvasRenderer.ts +0 -141
@@ -1,3 +1,4 @@
1
+ import Command from '../commands/Command';
1
2
  import Editor from '../Editor';
2
3
  import Rect2 from '../geometry/Rect2';
3
4
  import { Point2, Vec2 } from '../geometry/Vec2';
@@ -30,6 +31,7 @@ declare class Selection {
30
31
  private recomputeBoxRotation;
31
32
  getSelectedItemCount(): number;
32
33
  updateUI(): void;
34
+ deleteSelectedObjects(): Command;
33
35
  }
34
36
  export default class SelectionTool extends BaseTool {
35
37
  private editor;
@@ -45,5 +47,6 @@ export default class SelectionTool extends BaseTool {
45
47
  onGestureCancel(): void;
46
48
  setEnabled(enabled: boolean): void;
47
49
  getSelection(): Selection | null;
50
+ clearSelection(): void;
48
51
  }
49
52
  export {};
@@ -7,12 +7,12 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
7
7
  step((generator = generator.apply(thisArg, _arguments || [])).next());
8
8
  });
9
9
  };
10
+ import Erase from '../commands/Erase';
10
11
  import Mat33 from '../geometry/Mat33';
11
12
  // import Mat33 from "../geometry/Mat33";
12
13
  import Rect2 from '../geometry/Rect2';
13
14
  import { Vec2 } from '../geometry/Vec2';
14
15
  import { EditorEventType } from '../types';
15
- import Viewport from '../Viewport';
16
16
  import BaseTool from './BaseTool';
17
17
  import { ToolType } from './ToolController';
18
18
  const handleScreenSize = 30;
@@ -114,7 +114,7 @@ const makeDraggable = (element, onDrag, onDragEnd) => {
114
114
  element.addEventListener('pointercancel', onPointerEnd);
115
115
  };
116
116
  // Maximum number of strokes to transform without a re-render.
117
- const updateChunkSize = 50;
117
+ const updateChunkSize = 100;
118
118
  class Selection {
119
119
  constructor(startPoint, editor) {
120
120
  this.startPoint = startPoint;
@@ -284,10 +284,13 @@ class Selection {
284
284
  if (this.region.containsRect(elem.getBBox())) {
285
285
  return true;
286
286
  }
287
- else if (this.region.getEdges().some(edge => elem.intersects(edge))) {
288
- return true;
287
+ // Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
288
+ // As such, test with more lines than just this' edges.
289
+ const testLines = [];
290
+ for (const subregion of this.region.divideIntoGrid(2, 2)) {
291
+ testLines.push(...subregion.getEdges());
289
292
  }
290
- return false;
293
+ return testLines.some(edge => elem.intersects(edge));
291
294
  });
292
295
  // Find the bounding box of all selected elements.
293
296
  if (!this.recomputeRegion()) {
@@ -344,6 +347,9 @@ class Selection {
344
347
  this.backgroundBox.style.transform = `rotate(${rotationDeg}deg)`;
345
348
  this.rotateCircle.style.transform = `rotate(${-rotationDeg}deg)`;
346
349
  }
350
+ deleteSelectedObjects() {
351
+ return new Erase(this.selectedElems);
352
+ }
347
353
  }
348
354
  export default class SelectionTool extends BaseTool {
349
355
  constructor(editor, description) {
@@ -388,26 +394,9 @@ export default class SelectionTool extends BaseTool {
388
394
  tool: this,
389
395
  });
390
396
  if (hasSelection) {
391
- const visibleRect = this.editor.viewport.visibleRect;
392
- const selectionRect = this.selectionBox.region;
393
397
  this.editor.announceForAccessibility(this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount()));
394
- // Try to move the selection within the center 2/3rds of the viewport.
395
- const targetRect = visibleRect.transformedBoundingBox(Mat33.scaling2D(2 / 3, visibleRect.center));
396
- // Ensure that the selection fits within the target
397
- if (targetRect.w < selectionRect.w || targetRect.h < selectionRect.h) {
398
- const multiplier = Math.max(selectionRect.w / targetRect.w, selectionRect.h / targetRect.h);
399
- const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
400
- const viewportContentTransform = visibleRectTransform.inverse();
401
- (new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
402
- }
403
- // Ensure that the top left is visible
404
- if (!targetRect.containsRect(selectionRect)) {
405
- // target position - current position
406
- const translation = selectionRect.center.minus(targetRect.center);
407
- const visibleRectTransform = Mat33.translation(translation);
408
- const viewportContentTransform = visibleRectTransform.inverse();
409
- (new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
410
- }
398
+ const selectionRect = this.selectionBox.region;
399
+ this.editor.viewport.zoomTo(selectionRect).apply(this.editor);
411
400
  }
412
401
  }
413
402
  onPointerUp(event) {
@@ -434,4 +423,13 @@ export default class SelectionTool extends BaseTool {
434
423
  getSelection() {
435
424
  return this.selectionBox;
436
425
  }
426
+ clearSelection() {
427
+ this.handleOverlay.replaceChildren();
428
+ this.prevSelectionBox = this.selectionBox;
429
+ this.selectionBox = null;
430
+ this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
431
+ kind: EditorEventType.ToolUpdated,
432
+ tool: this,
433
+ });
434
+ }
437
435
  }
@@ -89,8 +89,9 @@ export interface ColorPickerToggled {
89
89
  export declare type EditorEventDataType = EditorToolEvent | EditorObjectEvent | EditorViewportChangedEvent | DisplayResizedEvent | EditorUndoStackUpdated | ColorPickerToggled;
90
90
  export declare type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null;
91
91
  export declare type ComponentAddedListener = (component: AbstractComponent) => void;
92
+ export declare type OnDetermineExportRectListener = (exportRect: Rect2) => void;
92
93
  export interface ImageLoader {
93
- start(onAddComponent: ComponentAddedListener, onProgressListener: OnProgressListener): Promise<Rect2>;
94
+ start(onAddComponent: ComponentAddedListener, onProgressListener: OnProgressListener, onDetermineExportRect?: OnDetermineExportRectListener): Promise<void>;
94
95
  }
95
96
  export interface StrokeDataPoint {
96
97
  pos: Point2;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.0.9",
3
+ "version": "0.1.1",
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/Editor.js",
6
6
  "types": "dist/src/Editor.d.ts",
package/src/Editor.ts CHANGED
@@ -9,9 +9,9 @@ import EventDispatcher from './EventDispatcher';
9
9
  import { Point2, Vec2 } from './geometry/Vec2';
10
10
  import Vec3 from './geometry/Vec3';
11
11
  import HTMLToolbar from './toolbar/HTMLToolbar';
12
- import { RenderablePathSpec } from './rendering/AbstractRenderer';
13
- import Display, { RenderingMode } from './Display';
14
- import SVGRenderer from './rendering/SVGRenderer';
12
+ import { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
13
+ import Display, { RenderingMode } from './rendering/Display';
14
+ import SVGRenderer from './rendering/renderers/SVGRenderer';
15
15
  import Color4 from './Color4';
16
16
  import SVGLoader from './SVGLoader';
17
17
  import Pointer from './Pointer';
@@ -311,6 +311,7 @@ export class Editor {
311
311
  private async asyncApplyOrUnapplyCommands(
312
312
  commands: Command[], apply: boolean, updateChunkSize: number
313
313
  ) {
314
+ this.display.setDraftMode(true);
314
315
  for (let i = 0; i < commands.length; i += updateChunkSize) {
315
316
  this.showLoadingWarning(i / commands.length);
316
317
 
@@ -332,6 +333,7 @@ export class Editor {
332
333
  });
333
334
  }
334
335
  }
336
+ this.display.setDraftMode(false);
335
337
  this.hideLoadingWarning();
336
338
  }
337
339
 
@@ -378,7 +380,8 @@ export class Editor {
378
380
  );
379
381
  }
380
382
 
381
- this.image.render(renderer, this.viewport);
383
+ //this.image.render(renderer, this.viewport);
384
+ this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
382
385
  this.rerenderQueued = false;
383
386
  }
384
387
 
@@ -461,22 +464,28 @@ export class Editor {
461
464
 
462
465
  public async loadFrom(loader: ImageLoader) {
463
466
  this.showLoadingWarning(0);
464
- const imageRect = await loader.start((component) => {
467
+ this.display.setDraftMode(true);
468
+
469
+ await loader.start((component) => {
465
470
  (new EditorImage.AddElementCommand(component)).apply(this);
466
471
  }, (countProcessed: number, totalToProcess: number) => {
467
- if (countProcessed % 100 === 0) {
472
+ if (countProcessed % 500 === 0) {
468
473
  this.showLoadingWarning(countProcessed / totalToProcess);
469
- this.rerender(false);
474
+ this.rerender();
470
475
  return new Promise(resolve => {
471
476
  requestAnimationFrame(() => resolve());
472
477
  });
473
478
  }
474
479
 
475
480
  return null;
481
+ }, (importExportRect: Rect2) => {
482
+ this.setImportExportRect(importExportRect).apply(this);
483
+ this.viewport.zoomTo(importExportRect).apply(this);
476
484
  });
477
485
  this.hideLoadingWarning();
478
486
 
479
- this.setImportExportRect(imageRect).apply(this);
487
+ this.display.setDraftMode(false);
488
+ this.queueRerender();
480
489
  }
481
490
 
482
491
  // Returns the size of the visible region of the output SVG
@@ -5,8 +5,8 @@ import Stroke from './components/Stroke';
5
5
  import { Vec2 } from './geometry/Vec2';
6
6
  import Path, { PathCommandType } from './geometry/Path';
7
7
  import Color4 from './Color4';
8
- import DummyRenderer from './rendering/DummyRenderer';
9
- import { RenderingStyle } from './rendering/AbstractRenderer';
8
+ import DummyRenderer from './rendering/renderers/DummyRenderer';
9
+ import { RenderingStyle } from './rendering/renderers/AbstractRenderer';
10
10
  import createEditor from './testing/createEditor';
11
11
 
12
12
  describe('EditorImage', () => {
@@ -1,10 +1,15 @@
1
1
  import Editor from './Editor';
2
- import AbstractRenderer from './rendering/AbstractRenderer';
2
+ import AbstractRenderer from './rendering/renderers/AbstractRenderer';
3
3
  import Command from './commands/Command';
4
4
  import Viewport from './Viewport';
5
5
  import AbstractComponent from './components/AbstractComponent';
6
6
  import Rect2 from './geometry/Rect2';
7
7
  import { EditorLocalization } from './localization';
8
+ import RenderingCache from './rendering/caching/RenderingCache';
9
+
10
+ export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
11
+ leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());
12
+ };
8
13
 
9
14
  // Handles lookup/storage of elements in the image
10
15
  export default class EditorImage {
@@ -20,7 +25,7 @@ export default class EditorImage {
20
25
 
21
26
  // Returns the parent of the given element, if it exists.
22
27
  public findParent(elem: AbstractComponent): ImageNode|null {
23
- const candidates = this.root.getLeavesInRegion(elem.getBBox());
28
+ const candidates = this.root.getLeavesIntersectingRegion(elem.getBBox());
24
29
  for (const candidate of candidates) {
25
30
  if (candidate.getContent() === elem) {
26
31
  return candidate;
@@ -29,25 +34,18 @@ export default class EditorImage {
29
34
  return null;
30
35
  }
31
36
 
32
- private sortLeaves(leaves: ImageNode[]) {
33
- leaves.sort((a, b) => a.getContent()!.zIndex - b.getContent()!.zIndex);
37
+ public renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport) {
38
+ cache.render(screenRenderer, this.root, viewport);
34
39
  }
35
40
 
36
- public render(renderer: AbstractRenderer, viewport: Viewport, minFraction: number = 0.001) {
37
- // Don't render components that are < 0.1% of the viewport.
38
- const leaves = this.root.getLeavesInRegion(viewport.visibleRect, minFraction);
39
- this.sortLeaves(leaves);
40
-
41
- for (const leaf of leaves) {
42
- // Leaves by definition have content
43
- leaf.getContent()!.render(renderer, viewport.visibleRect);
44
- }
41
+ public render(renderer: AbstractRenderer, viewport: Viewport) {
42
+ this.root.render(renderer, viewport.visibleRect);
45
43
  }
46
44
 
47
45
  // Renders all nodes, even ones not within the viewport
48
46
  public renderAll(renderer: AbstractRenderer) {
49
47
  const leaves = this.root.getLeaves();
50
- this.sortLeaves(leaves);
48
+ sortLeavesByZIndex(leaves);
51
49
 
52
50
  for (const leaf of leaves) {
53
51
  leaf.getContent()!.render(renderer, leaf.getBBox());
@@ -55,8 +53,9 @@ export default class EditorImage {
55
53
  }
56
54
 
57
55
  public getElementsIntersectingRegion(region: Rect2): AbstractComponent[] {
58
- const leaves = this.root.getLeavesInRegion(region);
59
- this.sortLeaves(leaves);
56
+ const leaves = this.root.getLeavesIntersectingRegion(region);
57
+ sortLeavesByZIndex(leaves);
58
+
60
59
  return leaves.map(leaf => leaf.getContent()!);
61
60
  }
62
61
 
@@ -100,15 +99,17 @@ export default class EditorImage {
100
99
  }
101
100
 
102
101
  export type AddElementCommand = typeof EditorImage.AddElementCommand.prototype;
102
+ type TooSmallToRenderCheck = (rect: Rect2)=> boolean;
103
103
 
104
-
104
+ // TODO: Assign leaf nodes to CacheNodes. When leaf nodes are modified, the corresponding CacheNodes can be updated.
105
105
  export class ImageNode {
106
106
  private content: AbstractComponent|null;
107
107
  private bbox: Rect2;
108
108
  private children: ImageNode[];
109
109
  private targetChildCount: number = 30;
110
- private minZIndex: number|null;
111
- private maxZIndex: number|null;
110
+
111
+ private id: number;
112
+ private static idCounter: number = 0;
112
113
 
113
114
  public constructor(
114
115
  private parent: ImageNode|null = null
@@ -117,8 +118,15 @@ export class ImageNode {
117
118
  this.bbox = Rect2.empty;
118
119
  this.content = null;
119
120
 
120
- this.minZIndex = null;
121
- this.maxZIndex = null;
121
+ this.id = ImageNode.idCounter++;
122
+ }
123
+
124
+ public getId() {
125
+ return this.id;
126
+ }
127
+
128
+ public onContentChange() {
129
+ this.id = ImageNode.idCounter++;
122
130
  }
123
131
 
124
132
  public getContent(): AbstractComponent|null {
@@ -129,18 +137,25 @@ export class ImageNode {
129
137
  return this.parent;
130
138
  }
131
139
 
132
- private getChildrenInRegion(region: Rect2): ImageNode[] {
140
+ private getChildrenIntersectingRegion(region: Rect2): ImageNode[] {
133
141
  return this.children.filter(child => {
134
142
  return child.getBBox().intersects(region);
135
143
  });
136
144
  }
137
145
 
146
+ public getChildrenOrSelfIntersectingRegion(region: Rect2): ImageNode[] {
147
+ if (this.content) {
148
+ return [this];
149
+ }
150
+ return this.getChildrenIntersectingRegion(region);
151
+ }
152
+
138
153
  // Returns a list of `ImageNode`s with content (and thus no children).
139
- public getLeavesInRegion(region: Rect2, minFractionOfRegion: number = 0): ImageNode[] {
154
+ public getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[] {
140
155
  const result: ImageNode[] = [];
141
156
 
142
157
  // Don't render if too small
143
- if (this.bbox.maxDimension / region.maxDimension <= minFractionOfRegion) {
158
+ if (isTooSmall?.(this.bbox)) {
144
159
  return [];
145
160
  }
146
161
 
@@ -148,9 +163,9 @@ export class ImageNode {
148
163
  result.push(this);
149
164
  }
150
165
 
151
- const children = this.getChildrenInRegion(region);
166
+ const children = this.getChildrenIntersectingRegion(region);
152
167
  for (const child of children) {
153
- result.push(...child.getLeavesInRegion(region, minFractionOfRegion));
168
+ result.push(...child.getLeavesIntersectingRegion(region, isTooSmall));
154
169
  }
155
170
 
156
171
  return result;
@@ -172,6 +187,8 @@ export class ImageNode {
172
187
  }
173
188
 
174
189
  public addLeaf(leaf: AbstractComponent): ImageNode {
190
+ this.onContentChange();
191
+
175
192
  if (this.content === null && this.children.length === 0) {
176
193
  this.content = leaf;
177
194
  this.recomputeBBox(true);
@@ -239,12 +256,8 @@ export class ImageNode {
239
256
  const oldBBox = this.bbox;
240
257
  if (this.content !== null) {
241
258
  this.bbox = this.content.getBBox();
242
- this.minZIndex = this.content.zIndex;
243
- this.maxZIndex = this.content.zIndex;
244
259
  } else {
245
260
  this.bbox = Rect2.empty;
246
- this.minZIndex = null;
247
- this.maxZIndex = null;
248
261
  let isFirst = true;
249
262
 
250
263
  for (const child of this.children) {
@@ -254,15 +267,6 @@ export class ImageNode {
254
267
  } else {
255
268
  this.bbox = this.bbox.union(child.getBBox());
256
269
  }
257
-
258
- this.minZIndex ??= child.minZIndex;
259
- this.maxZIndex ??= child.maxZIndex;
260
- if (child.minZIndex !== null && this.minZIndex !== null) {
261
- this.minZIndex = Math.min(child.minZIndex, this.minZIndex);
262
- }
263
- if (child.maxZIndex !== null && this.maxZIndex !== null) {
264
- this.maxZIndex = Math.max(child.maxZIndex, this.maxZIndex);
265
- }
266
270
  }
267
271
  }
268
272
 
@@ -295,9 +299,6 @@ export class ImageNode {
295
299
 
296
300
  // Remove this node and all of its children
297
301
  public remove() {
298
- this.minZIndex = null;
299
- this.maxZIndex = null;
300
-
301
302
  if (!this.parent) {
302
303
  this.content = null;
303
304
  this.children = [];
@@ -322,4 +323,15 @@ export class ImageNode {
322
323
  this.parent = null;
323
324
  this.children = [];
324
325
  }
326
+
327
+ public render(renderer: AbstractRenderer, visibleRect: Rect2) {
328
+ // Don't render components that are < 0.1% of the viewport.
329
+ const leaves = this.getLeavesIntersectingRegion(visibleRect, rect => renderer.isTooSmallToRender(rect));
330
+ sortLeavesByZIndex(leaves);
331
+
332
+ for (const leaf of leaves) {
333
+ // Leaves by definition have content
334
+ leaf.getContent()!.render(renderer, visibleRect);
335
+ }
336
+ }
325
337
  }
package/src/SVGLoader.ts CHANGED
@@ -5,8 +5,8 @@ import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
5
5
  import UnknownSVGObject from './components/UnknownSVGObject';
6
6
  import Path from './geometry/Path';
7
7
  import Rect2 from './geometry/Rect2';
8
- import { RenderablePathSpec, RenderingStyle } from './rendering/AbstractRenderer';
9
- import { ComponentAddedListener, ImageLoader, OnProgressListener } from './types';
8
+ import { RenderablePathSpec, RenderingStyle } from './rendering/renderers/AbstractRenderer';
9
+ import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
10
10
 
11
11
  type OnFinishListener = ()=> void;
12
12
 
@@ -16,6 +16,8 @@ export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
16
16
  export default class SVGLoader implements ImageLoader {
17
17
  private onAddComponent: ComponentAddedListener|null = null;
18
18
  private onProgress: OnProgressListener|null = null;
19
+ private onDetermineExportRect: OnDetermineExportRectListener|null = null;
20
+
19
21
  private processedCount: number = 0;
20
22
  private totalToProcess: number = 0;
21
23
  private rootViewBox: Rect2|null;
@@ -126,6 +128,7 @@ export default class SVGLoader implements ImageLoader {
126
128
  }
127
129
 
128
130
  this.rootViewBox = new Rect2(x, y, width, height);
131
+ this.onDetermineExportRect?.(this.rootViewBox);
129
132
  }
130
133
 
131
134
  private updateSVGAttrs(node: SVGSVGElement) {
@@ -172,10 +175,12 @@ export default class SVGLoader implements ImageLoader {
172
175
  }
173
176
 
174
177
  public async start(
175
- onAddComponent: ComponentAddedListener, onProgress: OnProgressListener
176
- ): Promise<Rect2> {
178
+ onAddComponent: ComponentAddedListener, onProgress: OnProgressListener,
179
+ onDetermineExportRect: OnDetermineExportRectListener|null = null
180
+ ): Promise<void> {
177
181
  this.onAddComponent = onAddComponent;
178
182
  this.onProgress = onProgress;
183
+ this.onDetermineExportRect = onDetermineExportRect;
179
184
 
180
185
  // Estimate the number of tags to process.
181
186
  this.totalToProcess = this.source.childElementCount;
@@ -185,14 +190,12 @@ export default class SVGLoader implements ImageLoader {
185
190
  await this.visit(this.source);
186
191
 
187
192
  const viewBox = this.rootViewBox;
188
- let result = defaultSVGViewRect;
189
193
 
190
- if (viewBox) {
191
- result = Rect2.of(viewBox);
194
+ if (!viewBox) {
195
+ this.onDetermineExportRect?.(defaultSVGViewRect);
192
196
  }
193
197
 
194
198
  this.onFinish?.();
195
- return result;
196
199
  }
197
200
 
198
201
  // TODO: Handling unsafe data! Tripple-check that this is secure!
package/src/Viewport.ts CHANGED
@@ -118,12 +118,20 @@ export class Viewport {
118
118
  return this.transform;
119
119
  }
120
120
 
121
+ public getResolution(): Vec2 {
122
+ return this.screenRect.size;
123
+ }
124
+
121
125
  // Returns the amount a vector on the canvas is scaled to become a vector on the screen.
122
126
  public getScaleFactor(): number {
123
127
  // Use transformVec3 to avoid translating the vector
124
128
  return this.transform.transformVec3(Vec3.unitX).magnitude();
125
129
  }
126
130
 
131
+ public getSizeOfPixelOnCanvas(): number {
132
+ return 1/this.getScaleFactor();
133
+ }
134
+
127
135
  // Returns the angle of the canvas in radians
128
136
  public getRotationAngle(): number {
129
137
  return this.transform.transformVec3(Vec3.unitX).angle();
@@ -158,6 +166,54 @@ export class Viewport {
158
166
  public roundPoint(point: Point2): Point2 {
159
167
  return Viewport.roundPoint(point, 1 / this.getScaleFactor());
160
168
  }
169
+
170
+ // Returns a Command that transforms the view such that [rect] is visible, and perhaps
171
+ // centered in the viewport.
172
+ // Returns null if no transformation is necessary
173
+ public zoomTo(toMakeVisible: Rect2): Command {
174
+ let transform = Mat33.identity;
175
+
176
+ // Try to move the selection within the center 2/3rds of the viewport.
177
+ const recomputeTargetRect = () => {
178
+ // transform transforms objects on the canvas. As such, we need to invert it
179
+ // to transform the viewport.
180
+ const visibleRect = this.visibleRect.transformedBoundingBox(transform.inverse());
181
+ return visibleRect.transformedBoundingBox(Mat33.scaling2D(2 / 3, visibleRect.center));
182
+ };
183
+
184
+ let targetRect = recomputeTargetRect();
185
+ const largerThanTarget = targetRect.w < toMakeVisible.w || targetRect.h < toMakeVisible.h;
186
+
187
+ // Ensure that toMakeVisible is at least 1/8th of the visible region.
188
+ const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
189
+
190
+ if (largerThanTarget || muchSmallerThanTarget) {
191
+ // If larger than the target, ensure that the longest axis is visible.
192
+ // If smaller, shrink the visible rectangle as much as possible
193
+ const multiplier = (largerThanTarget ? Math.max : Math.min)(
194
+ toMakeVisible.w / targetRect.w, toMakeVisible.h / targetRect.h
195
+ );
196
+ const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
197
+ const viewportContentTransform = visibleRectTransform.inverse();
198
+
199
+ transform = transform.rightMul(viewportContentTransform);
200
+ }
201
+
202
+ targetRect = recomputeTargetRect();
203
+
204
+ // Ensure that the center of the region is visible
205
+ if (!targetRect.containsRect(toMakeVisible)) {
206
+ // target position - current position
207
+ const translation = toMakeVisible.center.minus(targetRect.center);
208
+ const visibleRectTransform = Mat33.translation(translation);
209
+ const viewportContentTransform = visibleRectTransform.inverse();
210
+
211
+ transform = transform.rightMul(viewportContentTransform);
212
+ }
213
+
214
+
215
+ return new Viewport.ViewportTransform(transform);
216
+ }
161
217
  }
162
218
 
163
219
  export namespace Viewport { // eslint-disable-line
@@ -4,13 +4,13 @@ import EditorImage from '../EditorImage';
4
4
  import LineSegment2 from '../geometry/LineSegment2';
5
5
  import Mat33 from '../geometry/Mat33';
6
6
  import Rect2 from '../geometry/Rect2';
7
- import AbstractRenderer from '../rendering/AbstractRenderer';
7
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
8
8
  import { ImageComponentLocalization } from './localization';
9
9
 
10
10
  export default abstract class AbstractComponent {
11
11
  protected lastChangedTime: number;
12
12
  protected abstract contentBBox: Rect2;
13
- public zIndex: number;
13
+ private zIndex: number;
14
14
 
15
15
  // Topmost z-index
16
16
  private static zIndexCounter: number = 0;
@@ -20,6 +20,10 @@ export default abstract class AbstractComponent {
20
20
  this.zIndex = AbstractComponent.zIndexCounter++;
21
21
  }
22
22
 
23
+ public getZIndex(): number {
24
+ return this.zIndex;
25
+ }
26
+
23
27
  public getBBox(): Rect2 {
24
28
  return this.contentBBox;
25
29
  }
@@ -1,8 +1,8 @@
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';
5
- import SVGRenderer from '../rendering/SVGRenderer';
4
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
+ import SVGRenderer from '../rendering/renderers/SVGRenderer';
6
6
  import AbstractComponent from './AbstractComponent';
7
7
  import { ImageComponentLocalization } from './localization';
8
8
 
@@ -2,7 +2,7 @@ import LineSegment2 from '../geometry/LineSegment2';
2
2
  import Mat33 from '../geometry/Mat33';
3
3
  import Path from '../geometry/Path';
4
4
  import Rect2 from '../geometry/Rect2';
5
- import AbstractRenderer, { RenderablePathSpec, RenderingStyle } from '../rendering/AbstractRenderer';
5
+ import AbstractRenderer, { RenderablePathSpec, RenderingStyle } from '../rendering/renderers/AbstractRenderer';
6
6
  import AbstractComponent from './AbstractComponent';
7
7
  import { ImageComponentLocalization } from './localization';
8
8
 
@@ -1,8 +1,8 @@
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';
5
- import SVGRenderer from '../rendering/SVGRenderer';
4
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
+ import SVGRenderer from '../rendering/renderers/SVGRenderer';
6
6
  import AbstractComponent from './AbstractComponent';
7
7
  import { ImageComponentLocalization } from './localization';
8
8
 
@@ -1,6 +1,6 @@
1
1
  import { PathCommandType } from '../../geometry/Path';
2
2
  import Rect2 from '../../geometry/Rect2';
3
- import AbstractRenderer from '../../rendering/AbstractRenderer';
3
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
4
4
  import { StrokeDataPoint } from '../../types';
5
5
  import Viewport from '../../Viewport';
6
6
  import AbstractComponent from '../AbstractComponent';
@@ -1,5 +1,5 @@
1
1
  import { Bezier } from 'bezier-js';
2
- import AbstractRenderer, { RenderingStyle, RenderablePathSpec } from '../../rendering/AbstractRenderer';
2
+ import AbstractRenderer, { RenderingStyle, RenderablePathSpec } from '../../rendering/renderers/AbstractRenderer';
3
3
  import { Point2, Vec2 } from '../../geometry/Vec2';
4
4
  import Rect2 from '../../geometry/Rect2';
5
5
  import { PathCommand, PathCommandType } from '../../geometry/Path';
@@ -212,7 +212,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
212
212
  const lowerBoundary = computeBoundaryCurve(-1, halfVec);
213
213
 
214
214
  // If the boundaries have two intersections, increasing the half vector's length could fix this.
215
- if (upperBoundary.intersects(lowerBoundary).length === 2) {
215
+ if (upperBoundary.intersects(lowerBoundary).length > 0) {
216
216
  halfVec = halfVec.times(2);
217
217
  }
218
218
 
@@ -1,6 +1,6 @@
1
1
  import { PathCommandType } from '../../geometry/Path';
2
2
  import Rect2 from '../../geometry/Rect2';
3
- import AbstractRenderer from '../../rendering/AbstractRenderer';
3
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
4
4
  import { StrokeDataPoint } from '../../types';
5
5
  import Viewport from '../../Viewport';
6
6
  import AbstractComponent from '../AbstractComponent';
@@ -1,6 +1,6 @@
1
1
  import Path from '../../geometry/Path';
2
2
  import Rect2 from '../../geometry/Rect2';
3
- import AbstractRenderer from '../../rendering/AbstractRenderer';
3
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
4
4
  import { StrokeDataPoint } from '../../types';
5
5
  import Viewport from '../../Viewport';
6
6
  import AbstractComponent from '../AbstractComponent';