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
@@ -11,22 +11,23 @@ interface PinchData {
11
11
  dist: number;
12
12
  }
13
13
  export declare enum PanZoomMode {
14
- OneFingerGestures = 1,
15
- TwoFingerGestures = 2,
16
- AnyDevice = 4
14
+ OneFingerTouchGestures = 1,
15
+ TwoFingerTouchGestures = 2,
16
+ RightClickDrags = 4,
17
+ SinglePointerGestures = 8
17
18
  }
18
19
  export default class PanZoom extends BaseTool {
19
20
  private editor;
20
21
  private mode;
21
- readonly kind: ToolType.PanZoom | ToolType.TouchPanZoom;
22
+ readonly kind: ToolType.PanZoom;
22
23
  private transform;
23
24
  private lastAngle;
24
25
  private lastDist;
25
26
  private lastScreenCenter;
26
27
  constructor(editor: Editor, mode: PanZoomMode, description: string);
27
28
  computePinchData(p1: Pointer, p2: Pointer): PinchData;
28
- private pointersHaveCorrectDeviceType;
29
- onPointerDown({ allPointers }: PointerEvt): boolean;
29
+ private allPointersAreOfType;
30
+ onPointerDown({ allPointers: pointers }: PointerEvt): boolean;
30
31
  private getCenterDelta;
31
32
  private handleTwoFingerMove;
32
33
  private handleOneFingerMove;
@@ -36,5 +37,7 @@ export default class PanZoom extends BaseTool {
36
37
  private updateTransform;
37
38
  onWheel({ delta, screenPos }: WheelEvt): boolean;
38
39
  onKeyPress({ key }: KeyPressEvent): boolean;
40
+ setMode(mode: PanZoomMode): void;
41
+ getMode(): PanZoomMode;
39
42
  }
40
43
  export {};
@@ -2,17 +2,16 @@ import Mat33 from '../geometry/Mat33';
2
2
  import { Vec2 } from '../geometry/Vec2';
3
3
  import Vec3 from '../geometry/Vec3';
4
4
  import { PointerDevice } from '../Pointer';
5
+ import { EditorEventType } from '../types';
5
6
  import { Viewport } from '../Viewport';
6
7
  import BaseTool from './BaseTool';
7
8
  import { ToolType } from './ToolController';
8
9
  export var PanZoomMode;
9
10
  (function (PanZoomMode) {
10
- // Handle one-pointer gestures (touchscreen only unless AnyDevice is set)
11
- PanZoomMode[PanZoomMode["OneFingerGestures"] = 1] = "OneFingerGestures";
12
- // Handle two-pointer gestures (touchscreen only unless AnyDevice is set)
13
- PanZoomMode[PanZoomMode["TwoFingerGestures"] = 2] = "TwoFingerGestures";
14
- // / Handle gestures from any device, rather than just touch
15
- PanZoomMode[PanZoomMode["AnyDevice"] = 4] = "AnyDevice";
11
+ PanZoomMode[PanZoomMode["OneFingerTouchGestures"] = 1] = "OneFingerTouchGestures";
12
+ PanZoomMode[PanZoomMode["TwoFingerTouchGestures"] = 2] = "TwoFingerTouchGestures";
13
+ PanZoomMode[PanZoomMode["RightClickDrags"] = 4] = "RightClickDrags";
14
+ PanZoomMode[PanZoomMode["SinglePointerGestures"] = 8] = "SinglePointerGestures";
16
15
  })(PanZoomMode || (PanZoomMode = {}));
17
16
  export default class PanZoom extends BaseTool {
18
17
  constructor(editor, mode, description) {
@@ -21,9 +20,6 @@ export default class PanZoom extends BaseTool {
21
20
  this.mode = mode;
22
21
  this.kind = ToolType.PanZoom;
23
22
  this.transform = null;
24
- if (mode === PanZoomMode.OneFingerGestures) {
25
- this.kind = ToolType.TouchPanZoom;
26
- }
27
23
  }
28
24
  // Returns information about the pointers in a gesture
29
25
  computePinchData(p1, p2) {
@@ -34,24 +30,25 @@ export default class PanZoom extends BaseTool {
34
30
  const screenCenter = p2.screenPos.plus(p1.screenPos).times(0.5);
35
31
  return { canvasCenter, screenCenter, angle, dist };
36
32
  }
37
- pointersHaveCorrectDeviceType(pointers) {
38
- return this.mode & PanZoomMode.AnyDevice || pointers.every(pointer => pointer.device === PointerDevice.Touch);
33
+ allPointersAreOfType(pointers, kind) {
34
+ return pointers.every(pointer => pointer.device === kind);
39
35
  }
40
- onPointerDown({ allPointers }) {
36
+ onPointerDown({ allPointers: pointers }) {
41
37
  var _a;
42
38
  let handlingGesture = false;
43
- if (!this.pointersHaveCorrectDeviceType(allPointers)) {
44
- handlingGesture = false;
45
- }
46
- else if (allPointers.length === 2 && this.mode & PanZoomMode.TwoFingerGestures) {
47
- const { screenCenter, angle, dist } = this.computePinchData(allPointers[0], allPointers[1]);
39
+ const allAreTouch = this.allPointersAreOfType(pointers, PointerDevice.Touch);
40
+ const isRightClick = this.allPointersAreOfType(pointers, PointerDevice.RightButtonMouse);
41
+ if (allAreTouch && pointers.length === 2 && this.mode & PanZoomMode.TwoFingerTouchGestures) {
42
+ const { screenCenter, angle, dist } = this.computePinchData(pointers[0], pointers[1]);
48
43
  this.lastAngle = angle;
49
44
  this.lastDist = dist;
50
45
  this.lastScreenCenter = screenCenter;
51
46
  handlingGesture = true;
52
47
  }
53
- else if (allPointers.length === 1 && this.mode & PanZoomMode.OneFingerGestures) {
54
- this.lastScreenCenter = allPointers[0].screenPos;
48
+ else if (pointers.length === 1 && ((this.mode & PanZoomMode.OneFingerTouchGestures && allAreTouch)
49
+ || (isRightClick && this.mode & PanZoomMode.RightClickDrags)
50
+ || (this.mode & PanZoomMode.SinglePointerGestures))) {
51
+ this.lastScreenCenter = pointers[0].screenPos;
55
52
  handlingGesture = true;
56
53
  }
57
54
  if (handlingGesture) {
@@ -87,10 +84,10 @@ export default class PanZoom extends BaseTool {
87
84
  var _a;
88
85
  (_a = this.transform) !== null && _a !== void 0 ? _a : (this.transform = new Viewport.ViewportTransform(Mat33.identity));
89
86
  const lastTransform = this.transform;
90
- if (allPointers.length === 2 && this.mode & PanZoomMode.TwoFingerGestures) {
87
+ if (allPointers.length === 2) {
91
88
  this.handleTwoFingerMove(allPointers);
92
89
  }
93
- else if (allPointers.length === 1 && this.mode & PanZoomMode.OneFingerGestures) {
90
+ else if (allPointers.length === 1) {
94
91
  this.handleOneFingerMove(allPointers[0]);
95
92
  }
96
93
  lastTransform.unapply(this.editor);
@@ -191,4 +188,16 @@ export default class PanZoom extends BaseTool {
191
188
  this.updateTransform(transformUpdate);
192
189
  return true;
193
190
  }
191
+ setMode(mode) {
192
+ if (mode !== this.mode) {
193
+ this.mode = mode;
194
+ this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
195
+ kind: EditorEventType.ToolUpdated,
196
+ tool: this,
197
+ });
198
+ }
199
+ }
200
+ getMode() {
201
+ return this.mode;
202
+ }
194
203
  }
@@ -66,9 +66,14 @@ export default class Pen extends BaseTool {
66
66
  if (this.builder && current.isPrimary) {
67
67
  const stroke = this.builder.build();
68
68
  this.previewStroke();
69
- const canFlatten = true;
70
- const action = new EditorImage.AddElementCommand(stroke, canFlatten);
71
- this.editor.dispatch(action);
69
+ if (stroke.getBBox().area > 0) {
70
+ const canFlatten = true;
71
+ const action = new EditorImage.AddElementCommand(stroke, canFlatten);
72
+ this.editor.dispatch(action);
73
+ }
74
+ else {
75
+ console.warn('Pen: Not adding empty stroke', stroke, 'to the canvas.');
76
+ }
72
77
  }
73
78
  this.builder = null;
74
79
  this.editor.clearWetInk();
@@ -13,7 +13,6 @@ import Mat33 from '../geometry/Mat33';
13
13
  import Rect2 from '../geometry/Rect2';
14
14
  import { Vec2 } from '../geometry/Vec2';
15
15
  import { EditorEventType } from '../types';
16
- import Viewport from '../Viewport';
17
16
  import BaseTool from './BaseTool';
18
17
  import { ToolType } from './ToolController';
19
18
  const handleScreenSize = 30;
@@ -115,7 +114,7 @@ const makeDraggable = (element, onDrag, onDragEnd) => {
115
114
  element.addEventListener('pointercancel', onPointerEnd);
116
115
  };
117
116
  // Maximum number of strokes to transform without a re-render.
118
- const updateChunkSize = 50;
117
+ const updateChunkSize = 100;
119
118
  class Selection {
120
119
  constructor(startPoint, editor) {
121
120
  this.startPoint = startPoint;
@@ -285,10 +284,13 @@ class Selection {
285
284
  if (this.region.containsRect(elem.getBBox())) {
286
285
  return true;
287
286
  }
288
- else if (this.region.getEdges().some(edge => elem.intersects(edge))) {
289
- 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());
290
292
  }
291
- return false;
293
+ return testLines.some(edge => elem.intersects(edge));
292
294
  });
293
295
  // Find the bounding box of all selected elements.
294
296
  if (!this.recomputeRegion()) {
@@ -392,26 +394,9 @@ export default class SelectionTool extends BaseTool {
392
394
  tool: this,
393
395
  });
394
396
  if (hasSelection) {
395
- const visibleRect = this.editor.viewport.visibleRect;
396
- const selectionRect = this.selectionBox.region;
397
397
  this.editor.announceForAccessibility(this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount()));
398
- // Try to move the selection within the center 2/3rds of the viewport.
399
- const targetRect = visibleRect.transformedBoundingBox(Mat33.scaling2D(2 / 3, visibleRect.center));
400
- // Ensure that the selection fits within the target
401
- if (targetRect.w < selectionRect.w || targetRect.h < selectionRect.h) {
402
- const multiplier = Math.max(selectionRect.w / targetRect.w, selectionRect.h / targetRect.h);
403
- const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
404
- const viewportContentTransform = visibleRectTransform.inverse();
405
- (new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
406
- }
407
- // Ensure that the top left is visible
408
- if (!targetRect.containsRect(selectionRect)) {
409
- // target position - current position
410
- const translation = selectionRect.center.minus(targetRect.center);
411
- const visibleRectTransform = Mat33.translation(translation);
412
- const viewportContentTransform = visibleRectTransform.inverse();
413
- (new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
414
- }
398
+ const selectionRect = this.selectionBox.region;
399
+ this.editor.viewport.zoomTo(selectionRect).apply(this.editor);
415
400
  }
416
401
  }
417
402
  onPointerUp(event) {
@@ -3,12 +3,11 @@ import Editor from '../Editor';
3
3
  import BaseTool from './BaseTool';
4
4
  import { ToolLocalization } from './localization';
5
5
  export declare enum ToolType {
6
- TouchPanZoom = 0,
7
- Pen = 1,
8
- Selection = 2,
9
- Eraser = 3,
10
- PanZoom = 4,
11
- UndoRedoShortcut = 5
6
+ Pen = 0,
7
+ Selection = 1,
8
+ Eraser = 2,
9
+ PanZoom = 3,
10
+ UndoRedoShortcut = 4
12
11
  }
13
12
  export default class ToolController {
14
13
  private tools;
@@ -8,17 +8,16 @@ import Color4 from '../Color4';
8
8
  import UndoRedoShortcut from './UndoRedoShortcut';
9
9
  export var ToolType;
10
10
  (function (ToolType) {
11
- ToolType[ToolType["TouchPanZoom"] = 0] = "TouchPanZoom";
12
- ToolType[ToolType["Pen"] = 1] = "Pen";
13
- ToolType[ToolType["Selection"] = 2] = "Selection";
14
- ToolType[ToolType["Eraser"] = 3] = "Eraser";
15
- ToolType[ToolType["PanZoom"] = 4] = "PanZoom";
16
- ToolType[ToolType["UndoRedoShortcut"] = 5] = "UndoRedoShortcut";
11
+ ToolType[ToolType["Pen"] = 0] = "Pen";
12
+ ToolType[ToolType["Selection"] = 1] = "Selection";
13
+ ToolType[ToolType["Eraser"] = 2] = "Eraser";
14
+ ToolType[ToolType["PanZoom"] = 3] = "PanZoom";
15
+ ToolType[ToolType["UndoRedoShortcut"] = 4] = "UndoRedoShortcut";
17
16
  })(ToolType || (ToolType = {}));
18
17
  export default class ToolController {
19
18
  constructor(editor, localization) {
20
19
  const primaryToolEnabledGroup = new ToolEnabledGroup();
21
- const touchPanZoom = new PanZoom(editor, PanZoomMode.OneFingerGestures, localization.touchPanTool);
20
+ const panZoomTool = new PanZoom(editor, PanZoomMode.TwoFingerTouchGestures | PanZoomMode.RightClickDrags, localization.touchPanTool);
22
21
  const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness: 16 });
23
22
  const primaryTools = [
24
23
  new SelectionTool(editor, localization.selectionTool),
@@ -30,13 +29,12 @@ export default class ToolController {
30
29
  new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 64 }),
31
30
  ];
32
31
  this.tools = [
33
- touchPanZoom,
32
+ panZoomTool,
34
33
  ...primaryTools,
35
- new PanZoom(editor, PanZoomMode.TwoFingerGestures | PanZoomMode.AnyDevice, localization.twoFingerPanZoomTool),
36
34
  new UndoRedoShortcut(editor),
37
35
  ];
38
36
  primaryTools.forEach(tool => tool.setToolGroup(primaryToolEnabledGroup));
39
- touchPanZoom.setEnabled(false);
37
+ panZoomTool.setEnabled(true);
40
38
  primaryPenTool.setEnabled(true);
41
39
  editor.notifier.on(EditorEventType.ToolEnabled, event => {
42
40
  if (event.kind === EditorEventType.ToolEnabled) {
@@ -1,4 +1,5 @@
1
1
  export interface ToolLocalization {
2
+ RightClickDragPanTool: string;
2
3
  penTool: (penId: number) => string;
3
4
  selectionTool: string;
4
5
  eraserTool: string;
@@ -5,6 +5,7 @@ export const defaultToolLocalization = {
5
5
  touchPanTool: 'Touch Panning',
6
6
  twoFingerPanZoomTool: 'Panning and Zooming',
7
7
  undoRedoTool: 'Undo/Redo',
8
+ RightClickDragPanTool: 'Right-click drag',
8
9
  toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
9
10
  toolDisabledAnnouncement: (toolName) => `${toolName} disabled`,
10
11
  };
@@ -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.10",
3
+ "version": "0.1.2",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "main": "dist/src/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';
@@ -165,6 +165,10 @@ export class Editor {
165
165
  // May be required to prevent text selection on iOS/Safari:
166
166
  // See https://stackoverflow.com/a/70992717/17055750
167
167
  this.renderingRegion.addEventListener('touchstart', evt => evt.preventDefault());
168
+ this.renderingRegion.addEventListener('contextmenu', evt => {
169
+ // Don't show a context menu
170
+ evt.preventDefault();
171
+ });
168
172
 
169
173
  this.renderingRegion.addEventListener('pointerdown', evt => {
170
174
  const pointer = Pointer.ofEvent(evt, true, this.viewport);
@@ -380,7 +384,8 @@ export class Editor {
380
384
  );
381
385
  }
382
386
 
383
- this.image.render(renderer, this.viewport);
387
+ //this.image.render(renderer, this.viewport);
388
+ this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
384
389
  this.rerenderQueued = false;
385
390
  }
386
391
 
@@ -463,22 +468,28 @@ export class Editor {
463
468
 
464
469
  public async loadFrom(loader: ImageLoader) {
465
470
  this.showLoadingWarning(0);
466
- const imageRect = await loader.start((component) => {
471
+ this.display.setDraftMode(true);
472
+
473
+ await loader.start((component) => {
467
474
  (new EditorImage.AddElementCommand(component)).apply(this);
468
475
  }, (countProcessed: number, totalToProcess: number) => {
469
- if (countProcessed % 100 === 0) {
476
+ if (countProcessed % 500 === 0) {
470
477
  this.showLoadingWarning(countProcessed / totalToProcess);
471
- this.rerender(false);
478
+ this.rerender();
472
479
  return new Promise(resolve => {
473
480
  requestAnimationFrame(() => resolve());
474
481
  });
475
482
  }
476
483
 
477
484
  return null;
485
+ }, (importExportRect: Rect2) => {
486
+ this.setImportExportRect(importExportRect).apply(this);
487
+ this.viewport.zoomTo(importExportRect).apply(this);
478
488
  });
479
489
  this.hideLoadingWarning();
480
490
 
481
- this.setImportExportRect(imageRect).apply(this);
491
+ this.display.setDraftMode(false);
492
+ this.queueRerender();
482
493
  }
483
494
 
484
495
  // 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
 
@@ -74,6 +73,10 @@ export default class EditorImage {
74
73
  ) {
75
74
  this.#element = element;
76
75
  this.#applyByFlattening = applyByFlattening;
76
+
77
+ if (isNaN(this.#element.getBBox().area)) {
78
+ throw new Error('Elements in the image cannot have NaN bounding boxes');
79
+ }
77
80
  }
78
81
 
79
82
  public apply(editor: Editor) {
@@ -100,15 +103,17 @@ export default class EditorImage {
100
103
  }
101
104
 
102
105
  export type AddElementCommand = typeof EditorImage.AddElementCommand.prototype;
106
+ type TooSmallToRenderCheck = (rect: Rect2)=> boolean;
103
107
 
104
-
108
+ // TODO: Assign leaf nodes to CacheNodes. When leaf nodes are modified, the corresponding CacheNodes can be updated.
105
109
  export class ImageNode {
106
110
  private content: AbstractComponent|null;
107
111
  private bbox: Rect2;
108
112
  private children: ImageNode[];
109
113
  private targetChildCount: number = 30;
110
- private minZIndex: number|null;
111
- private maxZIndex: number|null;
114
+
115
+ private id: number;
116
+ private static idCounter: number = 0;
112
117
 
113
118
  public constructor(
114
119
  private parent: ImageNode|null = null
@@ -117,8 +122,15 @@ export class ImageNode {
117
122
  this.bbox = Rect2.empty;
118
123
  this.content = null;
119
124
 
120
- this.minZIndex = null;
121
- this.maxZIndex = null;
125
+ this.id = ImageNode.idCounter++;
126
+ }
127
+
128
+ public getId() {
129
+ return this.id;
130
+ }
131
+
132
+ public onContentChange() {
133
+ this.id = ImageNode.idCounter++;
122
134
  }
123
135
 
124
136
  public getContent(): AbstractComponent|null {
@@ -129,18 +141,25 @@ export class ImageNode {
129
141
  return this.parent;
130
142
  }
131
143
 
132
- private getChildrenInRegion(region: Rect2): ImageNode[] {
144
+ private getChildrenIntersectingRegion(region: Rect2): ImageNode[] {
133
145
  return this.children.filter(child => {
134
146
  return child.getBBox().intersects(region);
135
147
  });
136
148
  }
137
149
 
150
+ public getChildrenOrSelfIntersectingRegion(region: Rect2): ImageNode[] {
151
+ if (this.content) {
152
+ return [this];
153
+ }
154
+ return this.getChildrenIntersectingRegion(region);
155
+ }
156
+
138
157
  // Returns a list of `ImageNode`s with content (and thus no children).
139
- public getLeavesInRegion(region: Rect2, minFractionOfRegion: number = 0): ImageNode[] {
158
+ public getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[] {
140
159
  const result: ImageNode[] = [];
141
160
 
142
161
  // Don't render if too small
143
- if (this.bbox.maxDimension / region.maxDimension <= minFractionOfRegion) {
162
+ if (isTooSmall?.(this.bbox)) {
144
163
  return [];
145
164
  }
146
165
 
@@ -148,9 +167,9 @@ export class ImageNode {
148
167
  result.push(this);
149
168
  }
150
169
 
151
- const children = this.getChildrenInRegion(region);
170
+ const children = this.getChildrenIntersectingRegion(region);
152
171
  for (const child of children) {
153
- result.push(...child.getLeavesInRegion(region, minFractionOfRegion));
172
+ result.push(...child.getLeavesIntersectingRegion(region, isTooSmall));
154
173
  }
155
174
 
156
175
  return result;
@@ -172,6 +191,8 @@ export class ImageNode {
172
191
  }
173
192
 
174
193
  public addLeaf(leaf: AbstractComponent): ImageNode {
194
+ this.onContentChange();
195
+
175
196
  if (this.content === null && this.children.length === 0) {
176
197
  this.content = leaf;
177
198
  this.recomputeBBox(true);
@@ -239,12 +260,8 @@ export class ImageNode {
239
260
  const oldBBox = this.bbox;
240
261
  if (this.content !== null) {
241
262
  this.bbox = this.content.getBBox();
242
- this.minZIndex = this.content.zIndex;
243
- this.maxZIndex = this.content.zIndex;
244
263
  } else {
245
264
  this.bbox = Rect2.empty;
246
- this.minZIndex = null;
247
- this.maxZIndex = null;
248
265
  let isFirst = true;
249
266
 
250
267
  for (const child of this.children) {
@@ -254,15 +271,6 @@ export class ImageNode {
254
271
  } else {
255
272
  this.bbox = this.bbox.union(child.getBBox());
256
273
  }
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
274
  }
267
275
  }
268
276
 
@@ -295,9 +303,6 @@ export class ImageNode {
295
303
 
296
304
  // Remove this node and all of its children
297
305
  public remove() {
298
- this.minZIndex = null;
299
- this.maxZIndex = null;
300
-
301
306
  if (!this.parent) {
302
307
  this.content = null;
303
308
  this.children = [];
@@ -322,4 +327,15 @@ export class ImageNode {
322
327
  this.parent = null;
323
328
  this.children = [];
324
329
  }
330
+
331
+ public render(renderer: AbstractRenderer, visibleRect: Rect2) {
332
+ // Don't render components that are < 0.1% of the viewport.
333
+ const leaves = this.getLeavesIntersectingRegion(visibleRect, rect => renderer.isTooSmallToRender(rect));
334
+ sortLeavesByZIndex(leaves);
335
+
336
+ for (const leaf of leaves) {
337
+ // Leaves by definition have content
338
+ leaf.getContent()!.render(renderer, visibleRect);
339
+ }
340
+ }
325
341
  }
package/src/Pointer.ts CHANGED
@@ -5,7 +5,8 @@ export enum PointerDevice {
5
5
  Pen,
6
6
  Eraser,
7
7
  Touch,
8
- Mouse,
8
+ PrimaryButtonMouse,
9
+ RightButtonMouse,
9
10
  Other,
10
11
  }
11
12
 
@@ -31,7 +32,7 @@ export default class Pointer {
31
32
  public readonly id: number,
32
33
 
33
34
  // Numeric timestamp (milliseconds, as from (new Date).getTime())
34
- public readonly timeStamp: number
35
+ public readonly timeStamp: number,
35
36
  ) {
36
37
  }
37
38
 
@@ -39,7 +40,7 @@ export default class Pointer {
39
40
  const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
40
41
 
41
42
  const pointerTypeToDevice: Record<string, PointerDevice> = {
42
- 'mouse': PointerDevice.Mouse,
43
+ 'mouse': PointerDevice.PrimaryButtonMouse,
43
44
  'pen': PointerDevice.Pen,
44
45
  'touch': PointerDevice.Touch,
45
46
  };
@@ -53,6 +54,14 @@ export default class Pointer {
53
54
  const timeStamp = (new Date()).getTime();
54
55
  const canvasPos = viewport.roundPoint(viewport.screenToCanvas(screenPos));
55
56
 
57
+ if (device === PointerDevice.PrimaryButtonMouse) {
58
+ if (evt.buttons & 0x2) {
59
+ device = PointerDevice.RightButtonMouse;
60
+ } else if (!(evt.buttons & 0x1)) {
61
+ device = PointerDevice.Other;
62
+ }
63
+ }
64
+
56
65
  return new Pointer(
57
66
  screenPos,
58
67
  canvasPos,
@@ -61,7 +70,7 @@ export default class Pointer {
61
70
  isDown,
62
71
  device,
63
72
  evt.pointerId,
64
- timeStamp
73
+ timeStamp,
65
74
  );
66
75
  }
67
76