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.
- package/CHANGELOG.md +12 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +2 -2
- package/dist/src/Editor.js +15 -7
- package/dist/src/EditorImage.d.ts +15 -7
- package/dist/src/EditorImage.js +43 -37
- package/dist/src/SVGLoader.d.ts +3 -2
- package/dist/src/SVGLoader.js +9 -7
- package/dist/src/Viewport.d.ts +4 -0
- package/dist/src/Viewport.js +41 -0
- package/dist/src/components/AbstractComponent.d.ts +3 -2
- package/dist/src/components/AbstractComponent.js +3 -0
- package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -1
- package/dist/src/components/SVGGlobalAttributesObject.js +1 -1
- package/dist/src/components/Stroke.d.ts +1 -1
- package/dist/src/components/UnknownSVGObject.d.ts +1 -1
- package/dist/src/components/UnknownSVGObject.js +1 -1
- package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
- package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -1
- package/dist/src/components/builders/FreehandLineBuilder.js +1 -1
- package/dist/src/components/builders/LineBuilder.d.ts +1 -1
- package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
- package/dist/src/components/builders/types.d.ts +1 -1
- package/dist/src/geometry/Mat33.js +3 -0
- package/dist/src/geometry/Path.d.ts +1 -1
- package/dist/src/geometry/Path.js +5 -3
- package/dist/src/geometry/Rect2.d.ts +1 -0
- package/dist/src/geometry/Rect2.js +47 -9
- package/dist/src/{Display.d.ts → rendering/Display.d.ts} +6 -2
- package/dist/src/{Display.js → rendering/Display.js} +37 -4
- package/dist/src/rendering/caching/CacheRecord.d.ts +19 -0
- package/dist/src/rendering/caching/CacheRecord.js +52 -0
- package/dist/src/rendering/caching/CacheRecordManager.d.ts +11 -0
- package/dist/src/rendering/caching/CacheRecordManager.js +31 -0
- package/dist/src/rendering/caching/RenderingCache.d.ts +12 -0
- package/dist/src/rendering/caching/RenderingCache.js +42 -0
- package/dist/src/rendering/caching/RenderingCacheNode.d.ts +28 -0
- package/dist/src/rendering/caching/RenderingCacheNode.js +301 -0
- package/dist/src/rendering/caching/testUtils.d.ts +9 -0
- package/dist/src/rendering/caching/testUtils.js +20 -0
- package/dist/src/rendering/caching/types.d.ts +21 -0
- package/dist/src/rendering/caching/types.js +1 -0
- package/dist/src/rendering/{AbstractRenderer.d.ts → renderers/AbstractRenderer.d.ts} +19 -8
- package/dist/src/rendering/{AbstractRenderer.js → renderers/AbstractRenderer.js} +37 -2
- package/dist/src/rendering/{CanvasRenderer.d.ts → renderers/CanvasRenderer.d.ts} +14 -5
- package/dist/src/rendering/renderers/CanvasRenderer.js +164 -0
- package/dist/src/rendering/{DummyRenderer.d.ts → renderers/DummyRenderer.d.ts} +9 -5
- package/dist/src/rendering/{DummyRenderer.js → renderers/DummyRenderer.js} +35 -4
- package/dist/src/rendering/{SVGRenderer.d.ts → renderers/SVGRenderer.d.ts} +4 -3
- package/dist/src/rendering/{SVGRenderer.js → renderers/SVGRenderer.js} +14 -11
- package/dist/src/testing/createEditor.js +1 -1
- package/dist/src/toolbar/HTMLToolbar.js +11 -2
- package/dist/src/toolbar/localization.d.ts +1 -0
- package/dist/src/toolbar/localization.js +1 -0
- package/dist/src/tools/PanZoom.js +3 -0
- package/dist/src/tools/SelectionTool.d.ts +3 -0
- package/dist/src/tools/SelectionTool.js +22 -24
- package/dist/src/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/Editor.ts +17 -8
- package/src/EditorImage.test.ts +2 -2
- package/src/EditorImage.ts +54 -42
- package/src/SVGLoader.ts +11 -8
- package/src/Viewport.ts +56 -0
- package/src/components/AbstractComponent.ts +6 -2
- package/src/components/SVGGlobalAttributesObject.ts +2 -2
- package/src/components/Stroke.ts +1 -1
- package/src/components/UnknownSVGObject.ts +2 -2
- package/src/components/builders/ArrowBuilder.ts +1 -1
- package/src/components/builders/FreehandLineBuilder.ts +2 -2
- package/src/components/builders/LineBuilder.ts +1 -1
- package/src/components/builders/RectangleBuilder.ts +1 -1
- package/src/components/builders/types.ts +1 -1
- package/src/geometry/Mat33.ts +3 -0
- package/src/geometry/Path.toString.test.ts +12 -2
- package/src/geometry/Path.ts +8 -4
- package/src/geometry/Rect2.test.ts +47 -8
- package/src/geometry/Rect2.ts +57 -9
- package/src/{Display.ts → rendering/Display.ts} +43 -6
- package/src/rendering/caching/CacheRecord.test.ts +49 -0
- package/src/rendering/caching/CacheRecord.ts +73 -0
- package/src/rendering/caching/CacheRecordManager.ts +45 -0
- package/src/rendering/caching/RenderingCache.test.ts +44 -0
- package/src/rendering/caching/RenderingCache.ts +63 -0
- package/src/rendering/caching/RenderingCacheNode.ts +378 -0
- package/src/rendering/caching/testUtils.ts +35 -0
- package/src/rendering/caching/types.ts +39 -0
- package/src/rendering/{AbstractRenderer.ts → renderers/AbstractRenderer.ts} +57 -8
- package/src/rendering/renderers/CanvasRenderer.ts +219 -0
- package/src/rendering/renderers/DummyRenderer.test.ts +43 -0
- package/src/rendering/{DummyRenderer.ts → renderers/DummyRenderer.ts} +50 -7
- package/src/rendering/{SVGRenderer.ts → renderers/SVGRenderer.ts} +17 -13
- package/src/testing/createEditor.ts +1 -1
- package/src/toolbar/HTMLToolbar.ts +13 -2
- package/src/toolbar/localization.ts +2 -0
- package/src/tools/PanZoom.ts +3 -0
- package/src/tools/SelectionTool.test.ts +1 -1
- package/src/tools/SelectionTool.ts +28 -33
- package/src/types.ts +10 -3
- package/tsconfig.json +1 -0
- package/dist/__mocks__/coloris.d.ts +0 -2
- package/dist/__mocks__/coloris.js +0 -5
- package/dist/src/rendering/CanvasRenderer.js +0 -108
- 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
|
4
|
-
import
|
5
|
-
import
|
6
|
-
import
|
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
|
-
//
|
26
|
-
|
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 '
|
3
|
-
import Rect2 from '
|
4
|
-
import { Point2, Vec2 } from '
|
5
|
-
import Viewport from '
|
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.
|
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.
|
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.
|
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.
|
150
|
-
controlPoint2 = this.
|
151
|
-
endPoint = this.
|
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.
|
163
|
-
endPoint = this.
|
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
|
}
|
@@ -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: ',
|
package/src/tools/PanZoom.ts
CHANGED
@@ -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 =
|
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
|
-
|
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
|
-
|
490
|
-
|
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,
|
141
|
-
|
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