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
@@ -0,0 +1,219 @@
1
+ import Color4 from '../../Color4';
2
+ import Mat33 from '../../geometry/Mat33';
3
+ import Rect2 from '../../geometry/Rect2';
4
+ import { Point2, Vec2 } from '../../geometry/Vec2';
5
+ import Vec3 from '../../geometry/Vec3';
6
+ import Viewport from '../../Viewport';
7
+ import AbstractRenderer, { RenderablePathSpec, RenderingStyle } from './AbstractRenderer';
8
+
9
+ export default class CanvasRenderer extends AbstractRenderer {
10
+ private ignoreObjectsAboveLevel: number|null = null;
11
+ private ignoringObject: boolean = false;
12
+
13
+ // Minimum square distance of a control point from the line between the end points
14
+ // for the curve not to be drawn as a line.
15
+ // For example, if [minSquareCurveApproxDist] = 25 = 5², then a control point on a quadratic
16
+ // bezier curve needs to be at least 5 units away from the line between the curve's end points
17
+ // for the curve to be drawn as a Bezier curve (and not a line).
18
+ private minSquareCurveApproxDist: number;
19
+
20
+ // Minimum size of an object (in pixels) for it to be rendered.
21
+ private minRenderSizeAnyDimen: number;
22
+ private minRenderSizeBothDimens: number;
23
+
24
+ public constructor(private ctx: CanvasRenderingContext2D, viewport: Viewport) {
25
+ super(viewport);
26
+ this.setDraftMode(false);
27
+ }
28
+
29
+ public canRenderFromWithoutDataLoss(other: AbstractRenderer) {
30
+ return other instanceof CanvasRenderer;
31
+ }
32
+
33
+ public renderFromOtherOfSameType(transformBy: Mat33, other: AbstractRenderer): void {
34
+ if (!(other instanceof CanvasRenderer)) {
35
+ throw new Error(`${other} cannot be rendered onto ${this}`);
36
+ }
37
+ transformBy = this.getCanvasToScreenTransform().rightMul(transformBy);
38
+ this.ctx.save();
39
+ // From MDN, transform(a,b,c,d,e,f)
40
+ // takes input such that
41
+ // ⎡ a c e ⎤
42
+ // ⎢ b d f ⎥ transforms content drawn to [ctx].
43
+ // ⎣ 0 0 1 ⎦
44
+ this.ctx.transform(
45
+ transformBy.a1, transformBy.b1, // a, b
46
+ transformBy.a2, transformBy.b2, // c, d
47
+ transformBy.a3, transformBy.b3, // e, f
48
+ );
49
+ this.ctx.drawImage(other.ctx.canvas, 0, 0);
50
+ this.ctx.restore();
51
+ }
52
+
53
+ // Set parameters for lower/higher quality rendering
54
+ public setDraftMode(draftMode: boolean) {
55
+ if (draftMode) {
56
+ this.minSquareCurveApproxDist = 64;
57
+ this.minRenderSizeBothDimens = 8;
58
+ this.minRenderSizeAnyDimen = 2;
59
+ } else {
60
+ this.minSquareCurveApproxDist = 1;
61
+ this.minRenderSizeBothDimens = 1;
62
+ this.minRenderSizeAnyDimen = 0;
63
+ }
64
+ }
65
+
66
+ public displaySize(): Vec2 {
67
+ return Vec2.of(
68
+ this.ctx.canvas.clientWidth,
69
+ this.ctx.canvas.clientHeight
70
+ );
71
+ }
72
+
73
+ public clear() {
74
+ this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
75
+ }
76
+
77
+ protected beginPath(startPoint: Point2) {
78
+ startPoint = this.canvasToScreen(startPoint);
79
+
80
+ this.ctx.beginPath();
81
+ this.ctx.moveTo(startPoint.x, startPoint.y);
82
+ }
83
+
84
+ protected endPath(style: RenderingStyle) {
85
+ this.ctx.fillStyle = style.fill.toHexString();
86
+ this.ctx.fill();
87
+
88
+ if (style.stroke) {
89
+ this.ctx.strokeStyle = style.stroke.color.toHexString();
90
+ this.ctx.lineWidth = this.getSizeOfCanvasPixelOnScreen() * style.stroke.width;
91
+ this.ctx.stroke();
92
+ }
93
+
94
+ this.ctx.closePath();
95
+ }
96
+
97
+ protected lineTo(point: Point2) {
98
+ point = this.canvasToScreen(point);
99
+ this.ctx.lineTo(point.x, point.y);
100
+ }
101
+
102
+ protected moveTo(point: Point2) {
103
+ point = this.canvasToScreen(point);
104
+ this.ctx.moveTo(point.x, point.y);
105
+ }
106
+
107
+ protected traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2) {
108
+ p1 = this.canvasToScreen(p1);
109
+ p2 = this.canvasToScreen(p2);
110
+ p3 = this.canvasToScreen(p3);
111
+
112
+ // Approximate the curve if small enough.
113
+ const delta1 = p2.minus(p1);
114
+ const delta2 = p3.minus(p2);
115
+ if (delta1.magnitudeSquared() < this.minSquareCurveApproxDist
116
+ && delta2.magnitudeSquared() < this.minSquareCurveApproxDist) {
117
+ this.ctx.lineTo(p3.x, p3.y);
118
+ } else {
119
+ this.ctx.bezierCurveTo(p1.x, p1.y, p2.x, p2.y, p3.x, p3.y);
120
+ }
121
+ }
122
+
123
+ protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3) {
124
+ controlPoint = this.canvasToScreen(controlPoint);
125
+ endPoint = this.canvasToScreen(endPoint);
126
+
127
+ // Approximate the curve with a line if small enough
128
+ const delta = controlPoint.minus(endPoint);
129
+ if (delta.magnitudeSquared() < this.minSquareCurveApproxDist) {
130
+ this.ctx.lineTo(endPoint.x, endPoint.y);
131
+ } else {
132
+ this.ctx.quadraticCurveTo(
133
+ controlPoint.x, controlPoint.y, endPoint.x, endPoint.y
134
+ );
135
+ }
136
+ }
137
+
138
+ public drawPath(path: RenderablePathSpec) {
139
+ if (this.ignoringObject) {
140
+ return;
141
+ }
142
+
143
+ super.drawPath(path);
144
+ }
145
+
146
+ private clipLevels: number[] = [];
147
+ public startObject(boundingBox: Rect2, clip: boolean) {
148
+ if (this.isTooSmallToRender(boundingBox)) {
149
+ this.ignoreObjectsAboveLevel = this.getNestingLevel();
150
+ this.ignoringObject = true;
151
+ }
152
+
153
+ super.startObject(boundingBox);
154
+
155
+ if (!this.ignoringObject && clip) {
156
+ this.clipLevels.push(this.objectLevel);
157
+ this.ctx.save();
158
+ this.ctx.beginPath();
159
+ for (const corner of boundingBox.corners) {
160
+ const screenCorner = this.canvasToScreen(corner);
161
+ this.ctx.lineTo(screenCorner.x, screenCorner.y);
162
+ }
163
+ this.ctx.clip();
164
+ }
165
+ }
166
+
167
+ public endObject() {
168
+ if (!this.ignoringObject && this.clipLevels.length > 0) {
169
+ if (this.clipLevels[this.clipLevels.length - 1] === this.objectLevel) {
170
+ this.ctx.restore();
171
+ this.clipLevels.pop();
172
+ }
173
+ }
174
+
175
+ super.endObject();
176
+
177
+ // If exiting an object with a too-small-to-draw bounding box,
178
+ if (this.ignoreObjectsAboveLevel !== null && this.getNestingLevel() <= this.ignoreObjectsAboveLevel) {
179
+ this.ignoreObjectsAboveLevel = null;
180
+ this.ignoringObject = false;
181
+ }
182
+ }
183
+
184
+ public drawPoints(...points: Point2[]) {
185
+ const pointRadius = 10;
186
+
187
+ for (let i = 0; i < points.length; i++) {
188
+ const point = this.canvasToScreen(points[i]);
189
+
190
+ this.ctx.beginPath();
191
+ this.ctx.arc(point.x, point.y, pointRadius, 0, Math.PI * 2);
192
+ this.ctx.fillStyle = Color4.ofRGBA(
193
+ 0.5 + Math.sin(i) / 2,
194
+ 1.0,
195
+ 0.5 + Math.cos(i * 0.2) / 4, 0.5
196
+ ).toHexString();
197
+ this.ctx.fill();
198
+ this.ctx.stroke();
199
+ this.ctx.closePath();
200
+
201
+ this.ctx.textAlign = 'center';
202
+ this.ctx.textBaseline = 'middle';
203
+ this.ctx.fillStyle = 'black';
204
+ this.ctx.fillText(`${i}`, point.x, point.y, pointRadius * 2);
205
+ }
206
+ }
207
+
208
+ public isTooSmallToRender(rect: Rect2): boolean {
209
+ // Should we ignore all objects within this object's bbox?
210
+ const diagonal = this.getCanvasToScreenTransform().transformVec3(rect.size);
211
+
212
+ const bothDimenMinSize = this.minRenderSizeBothDimens;
213
+ const bothTooSmall = Math.abs(diagonal.x) < bothDimenMinSize && Math.abs(diagonal.y) < bothDimenMinSize;
214
+ const anyDimenMinSize = this.minRenderSizeAnyDimen;
215
+ const anyTooSmall = Math.abs(diagonal.x) < anyDimenMinSize || Math.abs(diagonal.y) < anyDimenMinSize;
216
+
217
+ return bothTooSmall || anyTooSmall;
218
+ }
219
+ }
@@ -0,0 +1,43 @@
1
+
2
+ import EventDispatcher from '../../EventDispatcher';
3
+ import Mat33 from '../../geometry/Mat33';
4
+ import { Vec2 } from '../../geometry/Vec2';
5
+ import Viewport from '../../Viewport';
6
+ import DummyRenderer from './DummyRenderer';
7
+
8
+ const makeRenderer = (): [DummyRenderer, Viewport] => {
9
+ const viewport = new Viewport(new EventDispatcher());
10
+ return [ new DummyRenderer(viewport), viewport ];
11
+ };
12
+
13
+ describe('DummyRenderer', () => {
14
+ it('should correctly calculate the size of a pixel on the screen', () => {
15
+ const [ renderer, viewport ] = makeRenderer();
16
+ viewport.updateScreenSize(Vec2.of(100, 100));
17
+ viewport.resetTransform(Mat33.identity);
18
+
19
+ expect(1/viewport.getScaleFactor()).toBe(1);
20
+ expect(renderer.getSizeOfCanvasPixelOnScreen()).toBe(1);
21
+
22
+ // Updating the translation matrix shouldn't affect the size of a pixel on the
23
+ // screen.
24
+ renderer.setTransform(Mat33.translation(Vec2.of(-1, -2)));
25
+ expect(renderer.getSizeOfCanvasPixelOnScreen()).toBe(1);
26
+ viewport.resetTransform(Mat33.translation(Vec2.of(3, 4)));
27
+ expect(renderer.getSizeOfCanvasPixelOnScreen()).toBe(1);
28
+
29
+ // Scale objects by a factor of 2 when drawing
30
+ renderer.setTransform(Mat33.scaling2D(2));
31
+ expect(renderer.getSizeOfCanvasPixelOnScreen()).toBe(2);
32
+ viewport.resetTransform(Mat33.scaling2D(0.5));
33
+
34
+ // When a renderer transform is set, **only** the renderer transform should be used.
35
+ expect(renderer.getSizeOfCanvasPixelOnScreen()).toBe(2);
36
+ renderer.setTransform(null);
37
+ expect(renderer.getSizeOfCanvasPixelOnScreen()).toBe(0.5);
38
+
39
+ // Rotating should not affect the size of a pixel
40
+ renderer.setTransform(Mat33.zRotation(Math.PI / 4).rightMul(Mat33.scaling2D(4)));
41
+ expect(renderer.getSizeOfCanvasPixelOnScreen()).toBe(4);
42
+ });
43
+ });
@@ -1,9 +1,10 @@
1
1
  // Renderer that outputs nothing. Useful for automated tests.
2
2
 
3
- import Rect2 from '../geometry/Rect2';
4
- import { Point2, Vec2 } from '../geometry/Vec2';
5
- import Vec3 from '../geometry/Vec3';
6
- import Viewport from '../Viewport';
3
+ import Mat33 from '../../geometry/Mat33';
4
+ import Rect2 from '../../geometry/Rect2';
5
+ import { Point2, Vec2 } from '../../geometry/Vec2';
6
+ import Vec3 from '../../geometry/Vec3';
7
+ import Viewport from '../../Viewport';
7
8
  import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
8
9
 
9
10
  export default class DummyRenderer extends AbstractRenderer {
@@ -22,8 +23,17 @@ export default class DummyRenderer extends AbstractRenderer {
22
23
  }
23
24
 
24
25
  public displaySize(): Vec2 {
25
- // Return a dummy
26
- return Vec2.of(640, 480);
26
+ // Do we have a stored viewport size?
27
+ const viewportSize = this.getViewport().getResolution();
28
+
29
+ // Don't use a 0x0 viewport — DummyRenderer is often used
30
+ // for tests that run without a display, so pretend we have a
31
+ // reasonable-sized display.
32
+ if (viewportSize.x === 0 || viewportSize.y === 0) {
33
+ return Vec2.of(640, 480);
34
+ }
35
+
36
+ return viewportSize;
27
37
  }
28
38
 
29
39
  public clear() {
@@ -47,18 +57,29 @@ export default class DummyRenderer extends AbstractRenderer {
47
57
  this.lastFillStyle = style;
48
58
  }
49
59
  protected lineTo(point: Vec3) {
60
+ point = this.canvasToScreen(point);
61
+
50
62
  this.lastPoint = point;
51
63
  this.pointBuffer.push(point);
52
64
  }
53
65
  protected moveTo(point: Point2) {
66
+ point = this.canvasToScreen(point);
67
+
54
68
  this.lastPoint = point;
55
69
  this.pointBuffer.push(point);
56
70
  }
57
71
  protected traceCubicBezierCurve(p1: Vec3, p2: Vec3, p3: Vec3) {
72
+ p1 = this.canvasToScreen(p1);
73
+ p2 = this.canvasToScreen(p2);
74
+ p3 = this.canvasToScreen(p3);
75
+
58
76
  this.lastPoint = p3;
59
77
  this.pointBuffer.push(p1, p2, p3);
60
78
  }
61
79
  protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3) {
80
+ controlPoint = this.canvasToScreen(controlPoint);
81
+ endPoint = this.canvasToScreen(endPoint);
82
+
62
83
  this.lastPoint = endPoint;
63
84
  this.pointBuffer.push(controlPoint, endPoint);
64
85
  }
@@ -67,7 +88,7 @@ export default class DummyRenderer extends AbstractRenderer {
67
88
  // As such, it is unlikely to be the target of automated tests.
68
89
  }
69
90
 
70
- public startObject(boundingBox: Rect2) {
91
+ public startObject(boundingBox: Rect2, _clip: boolean) {
71
92
  super.startObject(boundingBox);
72
93
 
73
94
  this.objectNestingLevel += 1;
@@ -77,4 +98,26 @@ export default class DummyRenderer extends AbstractRenderer {
77
98
 
78
99
  this.objectNestingLevel -= 1;
79
100
  }
101
+
102
+ public isTooSmallToRender(_rect: Rect2): boolean {
103
+ return false;
104
+ }
105
+
106
+
107
+ public canRenderFromWithoutDataLoss(other: AbstractRenderer) {
108
+ return other instanceof DummyRenderer;
109
+ }
110
+
111
+ public renderFromOtherOfSameType(transform: Mat33, other: AbstractRenderer): void {
112
+ if (!(other instanceof DummyRenderer)) {
113
+ throw new Error(`${other} cannot be rendered onto ${this}`);
114
+ }
115
+
116
+ this.renderedPathCount += other.renderedPathCount;
117
+ this.lastFillStyle = other.lastFillStyle;
118
+ this.lastPoint = other.lastPoint;
119
+ this.pointBuffer.push(...other.pointBuffer.map(point => {
120
+ return transform.transformVec2(point);
121
+ }));
122
+ }
80
123
  }
@@ -1,8 +1,8 @@
1
1
 
2
- import Path, { PathCommand, PathCommandType } from '../geometry/Path';
3
- import Rect2 from '../geometry/Rect2';
4
- import { Point2, Vec2 } from '../geometry/Vec2';
5
- import Viewport from '../Viewport';
2
+ import Path, { PathCommand, PathCommandType } from '../../geometry/Path';
3
+ import Rect2 from '../../geometry/Rect2';
4
+ import { Point2, Vec2 } from '../../geometry/Vec2';
5
+ import Viewport from '../../Viewport';
6
6
  import AbstractRenderer, { RenderingStyle } from './AbstractRenderer';
7
7
 
8
8
  const svgNameSpace = 'http://www.w3.org/2000/svg';
@@ -61,7 +61,7 @@ export default class SVGRenderer extends AbstractRenderer {
61
61
 
62
62
  protected beginPath(startPoint: Point2) {
63
63
  this.currentPath = [];
64
- this.pathStart = this.viewport.canvasToScreen(startPoint);
64
+ this.pathStart = this.canvasToScreen(startPoint);
65
65
  this.lastPathStart ??= this.pathStart;
66
66
  }
67
67
 
@@ -126,7 +126,7 @@ export default class SVGRenderer extends AbstractRenderer {
126
126
  }
127
127
 
128
128
  protected lineTo(point: Point2) {
129
- point = this.viewport.canvasToScreen(point);
129
+ point = this.canvasToScreen(point);
130
130
 
131
131
  this.currentPath!.push({
132
132
  kind: PathCommandType.LineTo,
@@ -135,7 +135,7 @@ export default class SVGRenderer extends AbstractRenderer {
135
135
  }
136
136
 
137
137
  protected moveTo(point: Point2) {
138
- point = this.viewport.canvasToScreen(point);
138
+ point = this.canvasToScreen(point);
139
139
 
140
140
  this.currentPath!.push({
141
141
  kind: PathCommandType.MoveTo,
@@ -146,9 +146,9 @@ export default class SVGRenderer extends AbstractRenderer {
146
146
  protected traceCubicBezierCurve(
147
147
  controlPoint1: Point2, controlPoint2: Point2, endPoint: Point2
148
148
  ) {
149
- controlPoint1 = this.viewport.canvasToScreen(controlPoint1);
150
- controlPoint2 = this.viewport.canvasToScreen(controlPoint2);
151
- endPoint = this.viewport.canvasToScreen(endPoint);
149
+ controlPoint1 = this.canvasToScreen(controlPoint1);
150
+ controlPoint2 = this.canvasToScreen(controlPoint2);
151
+ endPoint = this.canvasToScreen(endPoint);
152
152
 
153
153
  this.currentPath!.push({
154
154
  kind: PathCommandType.CubicBezierTo,
@@ -159,8 +159,8 @@ export default class SVGRenderer extends AbstractRenderer {
159
159
  }
160
160
 
161
161
  protected traceQuadraticBezierCurve(controlPoint: Point2, endPoint: Point2) {
162
- controlPoint = this.viewport.canvasToScreen(controlPoint);
163
- endPoint = this.viewport.canvasToScreen(endPoint);
162
+ controlPoint = this.canvasToScreen(controlPoint);
163
+ endPoint = this.canvasToScreen(endPoint);
164
164
 
165
165
  this.currentPath!.push({
166
166
  kind: PathCommandType.QuadraticBezierTo,
@@ -179,8 +179,12 @@ export default class SVGRenderer extends AbstractRenderer {
179
179
  });
180
180
  }
181
181
 
182
- // Renders a copy of the given element.
182
+ // Renders a **copy** of the given element.
183
183
  public drawSVGElem(elem: SVGElement) {
184
184
  this.elem.appendChild(elem.cloneNode(true));
185
185
  }
186
+
187
+ public isTooSmallToRender(_rect: Rect2): boolean {
188
+ return false;
189
+ }
186
190
  }
@@ -1,4 +1,4 @@
1
- import { RenderingMode } from '../Display';
1
+ import { RenderingMode } from '../rendering/Display';
2
2
  import Editor from '../Editor';
3
3
 
4
4
  export default () => new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });
@@ -10,7 +10,7 @@ import BaseTool from '../tools/BaseTool';
10
10
  import SelectionTool from '../tools/SelectionTool';
11
11
  import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
12
12
  import { Vec2 } from '../geometry/Vec2';
13
- import SVGRenderer from '../rendering/SVGRenderer';
13
+ import SVGRenderer from '../rendering/renderers/SVGRenderer';
14
14
  import Viewport from '../Viewport';
15
15
  import EventDispatcher from '../EventDispatcher';
16
16
  import { ComponentBuilderFactory } from '../components/builders/types';
@@ -245,15 +245,24 @@ class SelectionWidget extends ToolbarWidget {
245
245
  protected fillDropdown(dropdown: HTMLElement): boolean {
246
246
  const container = document.createElement('div');
247
247
  const resizeButton = document.createElement('button');
248
+ const deleteButton = document.createElement('button');
248
249
 
249
250
  resizeButton.innerText = this.localizationTable.resizeImageToSelection;
250
251
  resizeButton.disabled = true;
252
+ deleteButton.innerText = this.localizationTable.deleteSelection;
253
+ deleteButton.disabled = true;
251
254
 
252
255
  resizeButton.onclick = () => {
253
256
  const selection = this.tool.getSelection();
254
257
  this.editor.dispatch(this.editor.setImportExportRect(selection!.region));
255
258
  };
256
259
 
260
+ deleteButton.onclick = () => {
261
+ const selection = this.tool.getSelection();
262
+ this.editor.dispatch(selection!.deleteSelectedObjects());
263
+ this.tool.clearSelection();
264
+ };
265
+
257
266
  // Enable/disable actions based on whether items are selected
258
267
  this.editor.notifier.on(EditorEventType.ToolUpdated, toolEvt => {
259
268
  if (toolEvt.kind !== EditorEventType.ToolUpdated) {
@@ -263,11 +272,13 @@ class SelectionWidget extends ToolbarWidget {
263
272
  if (toolEvt.tool === this.tool) {
264
273
  const selection = this.tool.getSelection();
265
274
  const hasSelection = selection && selection.region.area > 0;
275
+
266
276
  resizeButton.disabled = !hasSelection;
277
+ deleteButton.disabled = resizeButton.disabled;
267
278
  }
268
279
  });
269
280
 
270
- container.replaceChildren(resizeButton);
281
+ container.replaceChildren(resizeButton, deleteButton);
271
282
  dropdown.appendChild(container);
272
283
  return true;
273
284
  }
@@ -14,6 +14,7 @@ export interface ToolbarLocalization {
14
14
  touchDrawing: string;
15
15
  thicknessLabel: string;
16
16
  resizeImageToSelection: string;
17
+ deleteSelection: string;
17
18
  undo: string;
18
19
  redo: string;
19
20
 
@@ -29,6 +30,7 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
29
30
  thicknessLabel: 'Thickness: ',
30
31
  colorLabel: 'Color: ',
31
32
  resizeImageToSelection: 'Resize image to selection',
33
+ deleteSelection: 'Delete selection',
32
34
  undo: 'Undo',
33
35
  redo: 'Redo',
34
36
  selectObjectType: 'Object type: ',
@@ -78,6 +78,7 @@ export default class PanZoom extends BaseTool {
78
78
 
79
79
  if (handlingGesture) {
80
80
  this.transform ??= new Viewport.ViewportTransform(Mat33.identity);
81
+ this.editor.display.setDraftMode(true);
81
82
  }
82
83
 
83
84
  return handlingGesture;
@@ -136,11 +137,13 @@ export default class PanZoom extends BaseTool {
136
137
  this.editor.dispatch(this.transform, false);
137
138
  }
138
139
 
140
+ this.editor.display.setDraftMode(false);
139
141
  this.transform = null;
140
142
  }
141
143
 
142
144
  public onGestureCancel(): void {
143
145
  this.transform?.unapply(this.editor);
146
+ this.editor.display.setDraftMode(false);
144
147
  this.transform = null;
145
148
  }
146
149
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  import Color4 from '../Color4';
4
4
  import Stroke from '../components/Stroke';
5
- import { RenderingMode } from '../Display';
5
+ import { RenderingMode } from '../rendering/Display';
6
6
  import Editor from '../Editor';
7
7
  import EditorImage from '../EditorImage';
8
8
  import Path from '../geometry/Path';
@@ -1,4 +1,5 @@
1
1
  import Command from '../commands/Command';
2
+ import Erase from '../commands/Erase';
2
3
  import AbstractComponent from '../components/AbstractComponent';
3
4
  import Editor from '../Editor';
4
5
  import Mat33 from '../geometry/Mat33';
@@ -6,7 +7,6 @@ import Mat33 from '../geometry/Mat33';
6
7
  import Rect2 from '../geometry/Rect2';
7
8
  import { Point2, Vec2 } from '../geometry/Vec2';
8
9
  import { EditorEventType, PointerEvt } from '../types';
9
- import Viewport from '../Viewport';
10
10
  import BaseTool from './BaseTool';
11
11
  import { ToolType } from './ToolController';
12
12
 
@@ -119,7 +119,7 @@ const makeDraggable = (element: HTMLElement, onDrag: DragCallback, onDragEnd: Dr
119
119
  };
120
120
 
121
121
  // Maximum number of strokes to transform without a re-render.
122
- const updateChunkSize = 50;
122
+ const updateChunkSize = 100;
123
123
 
124
124
  class Selection {
125
125
  public region: Rect2;
@@ -340,10 +340,16 @@ class Selection {
340
340
  this.selectedElems = this.editor.image.getElementsIntersectingRegion(this.region).filter(elem => {
341
341
  if (this.region.containsRect(elem.getBBox())) {
342
342
  return true;
343
- } else if (this.region.getEdges().some(edge => elem.intersects(edge))) {
344
- return true;
345
343
  }
346
- return false;
344
+
345
+ // Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
346
+ // As such, test with more lines than just this' edges.
347
+ const testLines = [];
348
+ for (const subregion of this.region.divideIntoGrid(2, 2)) {
349
+ testLines.push(...subregion.getEdges());
350
+ }
351
+
352
+ return testLines.some(edge => elem.intersects(edge));
347
353
  });
348
354
 
349
355
  // Find the bounding box of all selected elements.
@@ -421,6 +427,10 @@ class Selection {
421
427
  this.backgroundBox.style.transform = `rotate(${rotationDeg}deg)`;
422
428
  this.rotateCircle.style.transform = `rotate(${-rotationDeg}deg)`;
423
429
  }
430
+
431
+ public deleteSelectedObjects(): Command {
432
+ return new Erase(this.selectedElems);
433
+ }
424
434
  }
425
435
 
426
436
  export default class SelectionTool extends BaseTool {
@@ -479,38 +489,12 @@ export default class SelectionTool extends BaseTool {
479
489
  });
480
490
 
481
491
  if (hasSelection) {
482
- const visibleRect = this.editor.viewport.visibleRect;
483
- const selectionRect = this.selectionBox.region;
484
-
485
492
  this.editor.announceForAccessibility(
486
493
  this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount())
487
494
  );
488
495
 
489
- // Try to move the selection within the center 2/3rds of the viewport.
490
- const targetRect = visibleRect.transformedBoundingBox(
491
- Mat33.scaling2D(2 / 3, visibleRect.center)
492
- );
493
-
494
- // Ensure that the selection fits within the target
495
- if (targetRect.w < selectionRect.w || targetRect.h < selectionRect.h) {
496
- const multiplier = Math.max(
497
- selectionRect.w / targetRect.w, selectionRect.h / targetRect.h
498
- );
499
- const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
500
- const viewportContentTransform = visibleRectTransform.inverse();
501
-
502
- (new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
503
- }
504
-
505
- // Ensure that the top left is visible
506
- if (!targetRect.containsRect(selectionRect)) {
507
- // target position - current position
508
- const translation = selectionRect.center.minus(targetRect.center);
509
- const visibleRectTransform = Mat33.translation(translation);
510
- const viewportContentTransform = visibleRectTransform.inverse();
511
-
512
- (new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
513
- }
496
+ const selectionRect = this.selectionBox.region;
497
+ this.editor.viewport.zoomTo(selectionRect).apply(this.editor);
514
498
  }
515
499
  }
516
500
 
@@ -542,4 +526,15 @@ export default class SelectionTool extends BaseTool {
542
526
  public getSelection(): Selection|null {
543
527
  return this.selectionBox;
544
528
  }
529
+
530
+ public clearSelection() {
531
+ this.handleOverlay.replaceChildren();
532
+ this.prevSelectionBox = this.selectionBox;
533
+ this.selectionBox = null;
534
+
535
+ this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
536
+ kind: EditorEventType.ToolUpdated,
537
+ tool: this,
538
+ });
539
+ }
545
540
  }
package/src/types.ts CHANGED
@@ -134,11 +134,18 @@ export type OnProgressListener =
134
134
  (amountProcessed: number, totalToProcess: number)=> Promise<void>|null;
135
135
 
136
136
  export type ComponentAddedListener = (component: AbstractComponent)=> void;
137
+
138
+ // Called when a new estimate for the import/export rect has been generated. This can be called multiple times.
139
+ // Only the last call to this listener must be accurate.
140
+ // The import/export rect is also returned by [start].
141
+ export type OnDetermineExportRectListener = (exportRect: Rect2)=> void;
142
+
137
143
  export interface ImageLoader {
138
- // Returns the main region of the loaded image
139
144
  start(
140
- onAddComponent: ComponentAddedListener, onProgressListener: OnProgressListener
141
- ): Promise<Rect2>;
145
+ onAddComponent: ComponentAddedListener,
146
+ onProgressListener: OnProgressListener,
147
+ onDetermineExportRect?: OnDetermineExportRectListener,
148
+ ): Promise<void>;
142
149
  }
143
150
 
144
151
  export interface StrokeDataPoint {
package/tsconfig.json CHANGED
@@ -24,5 +24,6 @@
24
24
 
25
25
  // Files that don't need transpilation
26
26
  "**/*.test.ts",
27
+ "__mocks__/*"
27
28
  ],
28
29
  }
@@ -1,2 +0,0 @@
1
- export declare const coloris: (_options: any) => void;
2
- export declare const init: () => void;