js-draw 0.0.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/.eslintrc.js +57 -0
- package/.husky/pre-commit +4 -0
- package/LICENSE +21 -0
- package/README.md +74 -0
- package/__mocks__/coloris.ts +8 -0
- package/__mocks__/styleMock.js +1 -0
- package/dist/__mocks__/coloris.d.ts +2 -0
- package/dist/__mocks__/coloris.js +5 -0
- package/dist/build_tools/BundledFile.d.ts +12 -0
- package/dist/build_tools/BundledFile.js +153 -0
- package/dist/scripts/bundle.d.ts +1 -0
- package/dist/scripts/bundle.js +19 -0
- package/dist/scripts/watchBundle.d.ts +1 -0
- package/dist/scripts/watchBundle.js +9 -0
- package/dist/src/Color4.d.ts +23 -0
- package/dist/src/Color4.js +102 -0
- package/dist/src/Display.d.ts +22 -0
- package/dist/src/Display.js +93 -0
- package/dist/src/Editor.d.ts +55 -0
- package/dist/src/Editor.js +366 -0
- package/dist/src/EditorImage.d.ts +44 -0
- package/dist/src/EditorImage.js +243 -0
- package/dist/src/EventDispatcher.d.ts +11 -0
- package/dist/src/EventDispatcher.js +39 -0
- package/dist/src/Pointer.d.ts +22 -0
- package/dist/src/Pointer.js +57 -0
- package/dist/src/SVGLoader.d.ts +21 -0
- package/dist/src/SVGLoader.js +204 -0
- package/dist/src/StrokeBuilder.d.ts +35 -0
- package/dist/src/StrokeBuilder.js +275 -0
- package/dist/src/UndoRedoHistory.d.ts +17 -0
- package/dist/src/UndoRedoHistory.js +46 -0
- package/dist/src/Viewport.d.ts +39 -0
- package/dist/src/Viewport.js +134 -0
- package/dist/src/commands/Command.d.ts +15 -0
- package/dist/src/commands/Command.js +29 -0
- package/dist/src/commands/Erase.d.ts +11 -0
- package/dist/src/commands/Erase.js +37 -0
- package/dist/src/commands/localization.d.ts +19 -0
- package/dist/src/commands/localization.js +17 -0
- package/dist/src/components/AbstractComponent.d.ts +19 -0
- package/dist/src/components/AbstractComponent.js +46 -0
- package/dist/src/components/Stroke.d.ts +16 -0
- package/dist/src/components/Stroke.js +79 -0
- package/dist/src/components/UnknownSVGObject.d.ts +15 -0
- package/dist/src/components/UnknownSVGObject.js +25 -0
- package/dist/src/components/localization.d.ts +5 -0
- package/dist/src/components/localization.js +4 -0
- package/dist/src/geometry/LineSegment2.d.ts +19 -0
- package/dist/src/geometry/LineSegment2.js +100 -0
- package/dist/src/geometry/Mat33.d.ts +31 -0
- package/dist/src/geometry/Mat33.js +187 -0
- package/dist/src/geometry/Path.d.ts +55 -0
- package/dist/src/geometry/Path.js +364 -0
- package/dist/src/geometry/Rect2.d.ts +47 -0
- package/dist/src/geometry/Rect2.js +148 -0
- package/dist/src/geometry/Vec2.d.ts +13 -0
- package/dist/src/geometry/Vec2.js +13 -0
- package/dist/src/geometry/Vec3.d.ts +32 -0
- package/dist/src/geometry/Vec3.js +98 -0
- package/dist/src/localization.d.ts +12 -0
- package/dist/src/localization.js +5 -0
- package/dist/src/main.d.ts +3 -0
- package/dist/src/main.js +4 -0
- package/dist/src/rendering/AbstractRenderer.d.ts +38 -0
- package/dist/src/rendering/AbstractRenderer.js +108 -0
- package/dist/src/rendering/CanvasRenderer.d.ts +23 -0
- package/dist/src/rendering/CanvasRenderer.js +108 -0
- package/dist/src/rendering/DummyRenderer.d.ts +25 -0
- package/dist/src/rendering/DummyRenderer.js +65 -0
- package/dist/src/rendering/SVGRenderer.d.ts +27 -0
- package/dist/src/rendering/SVGRenderer.js +122 -0
- package/dist/src/testing/loadExpectExtensions.d.ts +17 -0
- package/dist/src/testing/loadExpectExtensions.js +27 -0
- package/dist/src/toolbar/HTMLToolbar.d.ts +12 -0
- package/dist/src/toolbar/HTMLToolbar.js +444 -0
- package/dist/src/toolbar/types.d.ts +17 -0
- package/dist/src/toolbar/types.js +5 -0
- package/dist/src/tools/BaseTool.d.ts +20 -0
- package/dist/src/tools/BaseTool.js +44 -0
- package/dist/src/tools/Eraser.d.ts +16 -0
- package/dist/src/tools/Eraser.js +53 -0
- package/dist/src/tools/PanZoom.d.ts +40 -0
- package/dist/src/tools/PanZoom.js +191 -0
- package/dist/src/tools/Pen.d.ts +25 -0
- package/dist/src/tools/Pen.js +97 -0
- package/dist/src/tools/SelectionTool.d.ts +49 -0
- package/dist/src/tools/SelectionTool.js +437 -0
- package/dist/src/tools/ToolController.d.ts +18 -0
- package/dist/src/tools/ToolController.js +110 -0
- package/dist/src/tools/ToolEnabledGroup.d.ts +6 -0
- package/dist/src/tools/ToolEnabledGroup.js +11 -0
- package/dist/src/tools/localization.d.ts +10 -0
- package/dist/src/tools/localization.js +9 -0
- package/dist/src/types.d.ts +88 -0
- package/dist/src/types.js +20 -0
- package/jest.config.js +22 -0
- package/lint-staged.config.js +6 -0
- package/package.json +82 -0
- package/src/Color4.test.ts +12 -0
- package/src/Color4.ts +122 -0
- package/src/Display.ts +118 -0
- package/src/Editor.css +58 -0
- package/src/Editor.ts +469 -0
- package/src/EditorImage.test.ts +90 -0
- package/src/EditorImage.ts +297 -0
- package/src/EventDispatcher.test.ts +123 -0
- package/src/EventDispatcher.ts +53 -0
- package/src/Pointer.ts +93 -0
- package/src/SVGLoader.ts +230 -0
- package/src/StrokeBuilder.ts +362 -0
- package/src/UndoRedoHistory.ts +61 -0
- package/src/Viewport.ts +168 -0
- package/src/commands/Command.ts +43 -0
- package/src/commands/Erase.ts +52 -0
- package/src/commands/localization.ts +38 -0
- package/src/components/AbstractComponent.ts +73 -0
- package/src/components/Stroke.test.ts +18 -0
- package/src/components/Stroke.ts +102 -0
- package/src/components/UnknownSVGObject.ts +36 -0
- package/src/components/localization.ts +9 -0
- package/src/editorStyles.js +3 -0
- package/src/geometry/LineSegment2.test.ts +77 -0
- package/src/geometry/LineSegment2.ts +127 -0
- package/src/geometry/Mat33.test.ts +144 -0
- package/src/geometry/Mat33.ts +268 -0
- package/src/geometry/Path.fromString.test.ts +146 -0
- package/src/geometry/Path.test.ts +96 -0
- package/src/geometry/Path.toString.test.ts +31 -0
- package/src/geometry/Path.ts +456 -0
- package/src/geometry/Rect2.test.ts +121 -0
- package/src/geometry/Rect2.ts +215 -0
- package/src/geometry/Vec2.test.ts +32 -0
- package/src/geometry/Vec2.ts +18 -0
- package/src/geometry/Vec3.test.ts +29 -0
- package/src/geometry/Vec3.ts +133 -0
- package/src/localization.ts +27 -0
- package/src/rendering/AbstractRenderer.ts +164 -0
- package/src/rendering/CanvasRenderer.ts +141 -0
- package/src/rendering/DummyRenderer.ts +80 -0
- package/src/rendering/SVGRenderer.ts +159 -0
- package/src/testing/loadExpectExtensions.ts +43 -0
- package/src/toolbar/HTMLToolbar.ts +551 -0
- package/src/toolbar/toolbar.css +110 -0
- package/src/toolbar/types.ts +20 -0
- package/src/tools/BaseTool.ts +58 -0
- package/src/tools/Eraser.ts +67 -0
- package/src/tools/PanZoom.ts +253 -0
- package/src/tools/Pen.ts +121 -0
- package/src/tools/SelectionTool.test.ts +85 -0
- package/src/tools/SelectionTool.ts +545 -0
- package/src/tools/ToolController.ts +126 -0
- package/src/tools/ToolEnabledGroup.ts +14 -0
- package/src/tools/localization.ts +22 -0
- package/src/types.ts +133 -0
- package/tsconfig.json +28 -0
@@ -0,0 +1,437 @@
|
|
1
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
2
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
3
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
4
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
5
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
6
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
7
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
8
|
+
});
|
9
|
+
};
|
10
|
+
import Mat33 from '../geometry/Mat33';
|
11
|
+
// import Mat33 from "../geometry/Mat33";
|
12
|
+
import Rect2 from '../geometry/Rect2';
|
13
|
+
import { Vec2 } from '../geometry/Vec2';
|
14
|
+
import { EditorEventType } from '../types';
|
15
|
+
import Viewport from '../Viewport';
|
16
|
+
import BaseTool from './BaseTool';
|
17
|
+
import { ToolType } from './ToolController';
|
18
|
+
const handleScreenSize = 30;
|
19
|
+
const styles = `
|
20
|
+
.handleOverlay {
|
21
|
+
}
|
22
|
+
|
23
|
+
.handleOverlay > .selectionBox {
|
24
|
+
position: fixed;
|
25
|
+
z-index: 0;
|
26
|
+
transform-origin: center;
|
27
|
+
}
|
28
|
+
|
29
|
+
.handleOverlay > .selectionBox .draggableBackground {
|
30
|
+
position: absolute;
|
31
|
+
top: 0;
|
32
|
+
left: 0;
|
33
|
+
right: 0;
|
34
|
+
bottom: 0;
|
35
|
+
|
36
|
+
background-color: var(--secondary-background-color);
|
37
|
+
opacity: 0.8;
|
38
|
+
border: 1px solid var(--primary-background-color);
|
39
|
+
}
|
40
|
+
|
41
|
+
.handleOverlay .resizeCorner {
|
42
|
+
width: ${handleScreenSize}px;
|
43
|
+
height: ${handleScreenSize}px;
|
44
|
+
margin-right: -${handleScreenSize / 2}px;
|
45
|
+
margin-bottom: -${handleScreenSize / 2}px;
|
46
|
+
|
47
|
+
position: absolute;
|
48
|
+
bottom: 0;
|
49
|
+
right: 0;
|
50
|
+
|
51
|
+
opacity: 0.8;
|
52
|
+
background-color: var(--primary-background-color);
|
53
|
+
border: 1px solid var(--primary-foreground-color);
|
54
|
+
}
|
55
|
+
|
56
|
+
.handleOverlay > .selectionBox .rotateCircleContainer {
|
57
|
+
position: absolute;
|
58
|
+
top: 50%;
|
59
|
+
bottom: 50%;
|
60
|
+
left: 50%;
|
61
|
+
right: 50%;
|
62
|
+
}
|
63
|
+
|
64
|
+
.handleOverlay .rotateCircle {
|
65
|
+
width: ${handleScreenSize}px;
|
66
|
+
height: ${handleScreenSize}px;
|
67
|
+
margin-left: -${handleScreenSize / 2}px;
|
68
|
+
margin-top: -${handleScreenSize / 2}px;
|
69
|
+
opacity: 0.8;
|
70
|
+
|
71
|
+
border: 1px solid var(--primary-foreground-color);
|
72
|
+
background-color: var(--primary-background-color);
|
73
|
+
border-radius: 100%;
|
74
|
+
}
|
75
|
+
`;
|
76
|
+
const makeDraggable = (element, onDrag, onDragEnd) => {
|
77
|
+
element.style.touchAction = 'none';
|
78
|
+
let down = false;
|
79
|
+
// Work around a Safari bug
|
80
|
+
element.addEventListener('touchstart', evt => evt.preventDefault());
|
81
|
+
let lastX;
|
82
|
+
let lastY;
|
83
|
+
element.addEventListener('pointerdown', event => {
|
84
|
+
if (event.isPrimary) {
|
85
|
+
down = true;
|
86
|
+
element.setPointerCapture(event.pointerId);
|
87
|
+
lastX = event.pageX;
|
88
|
+
lastY = event.pageY;
|
89
|
+
return true;
|
90
|
+
}
|
91
|
+
return false;
|
92
|
+
});
|
93
|
+
element.addEventListener('pointermove', event => {
|
94
|
+
if (event.isPrimary && down) {
|
95
|
+
// Safari/iOS doesn't seem to support movementX/movementY on pointer events.
|
96
|
+
// Calculate manually:
|
97
|
+
const delta = Vec2.of(event.pageX - lastX, event.pageY - lastY);
|
98
|
+
onDrag(delta, Vec2.of(event.offsetX, event.offsetY));
|
99
|
+
lastX = event.pageX;
|
100
|
+
lastY = event.pageY;
|
101
|
+
return true;
|
102
|
+
}
|
103
|
+
return false;
|
104
|
+
});
|
105
|
+
const onPointerEnd = (event) => {
|
106
|
+
if (event.isPrimary) {
|
107
|
+
down = false;
|
108
|
+
onDragEnd();
|
109
|
+
return true;
|
110
|
+
}
|
111
|
+
return false;
|
112
|
+
};
|
113
|
+
element.addEventListener('pointerup', onPointerEnd);
|
114
|
+
element.addEventListener('pointercancel', onPointerEnd);
|
115
|
+
};
|
116
|
+
// Maximum number of strokes to transform without a re-render.
|
117
|
+
const updateChunkSize = 50;
|
118
|
+
class Selection {
|
119
|
+
constructor(startPoint, editor) {
|
120
|
+
this.startPoint = startPoint;
|
121
|
+
this.editor = editor;
|
122
|
+
this.boxRotation = this.editor.viewport.getRotationAngle();
|
123
|
+
this.selectedElems = [];
|
124
|
+
this.region = Rect2.bboxOf([startPoint]);
|
125
|
+
// Create draggable rectangles
|
126
|
+
this.backgroundBox = document.createElement('div');
|
127
|
+
const draggableBackground = document.createElement('div');
|
128
|
+
const resizeCorner = document.createElement('div');
|
129
|
+
this.rotateCircle = document.createElement('div');
|
130
|
+
const rotateCircleContainer = document.createElement('div');
|
131
|
+
this.backgroundBox.classList.add('selectionBox');
|
132
|
+
draggableBackground.classList.add('draggableBackground');
|
133
|
+
resizeCorner.classList.add('resizeCorner');
|
134
|
+
this.rotateCircle.classList.add('rotateCircle');
|
135
|
+
rotateCircleContainer.classList.add('rotateCircleContainer');
|
136
|
+
rotateCircleContainer.appendChild(this.rotateCircle);
|
137
|
+
this.backgroundBox.appendChild(draggableBackground);
|
138
|
+
this.backgroundBox.appendChild(rotateCircleContainer);
|
139
|
+
this.backgroundBox.appendChild(resizeCorner);
|
140
|
+
this.transformationCommands = [];
|
141
|
+
this.transform = Mat33.identity;
|
142
|
+
makeDraggable(draggableBackground, (deltaPosition) => {
|
143
|
+
this.handleBackgroundDrag(deltaPosition);
|
144
|
+
}, () => this.finishDragging());
|
145
|
+
makeDraggable(resizeCorner, (deltaPosition) => {
|
146
|
+
this.handleResizeCornerDrag(deltaPosition);
|
147
|
+
}, () => this.finishDragging());
|
148
|
+
makeDraggable(this.rotateCircle, (_deltaPosition, offset) => {
|
149
|
+
this.handleRotateCircleDrag(offset);
|
150
|
+
}, () => this.finishDragging());
|
151
|
+
}
|
152
|
+
// Note a small change in the position of this' background while dragging
|
153
|
+
// At the end of a drag, changes should be applied by calling this.finishDragging()
|
154
|
+
handleBackgroundDrag(deltaPosition) {
|
155
|
+
// Re-scale the change in position
|
156
|
+
// (use a Vec3 transform to avoid translating deltaPosition)
|
157
|
+
deltaPosition = this.editor.viewport.screenToCanvasTransform.transformVec3(deltaPosition);
|
158
|
+
// Snap position to a multiple of 10 (additional decimal points lead to larger files).
|
159
|
+
deltaPosition = this.editor.viewport.roundPoint(deltaPosition);
|
160
|
+
this.region = this.region.translatedBy(deltaPosition);
|
161
|
+
this.transform = this.transform.rightMul(Mat33.translation(deltaPosition));
|
162
|
+
this.previewTransformCmds();
|
163
|
+
}
|
164
|
+
handleResizeCornerDrag(deltaPosition) {
|
165
|
+
deltaPosition = this.editor.viewport.screenToCanvasTransform.transformVec3(deltaPosition);
|
166
|
+
deltaPosition = this.editor.viewport.roundPoint(deltaPosition);
|
167
|
+
const oldWidth = this.region.w;
|
168
|
+
const oldHeight = this.region.h;
|
169
|
+
const newSize = this.region.size.plus(deltaPosition);
|
170
|
+
if (newSize.y > 0 && newSize.x > 0) {
|
171
|
+
this.region = this.region.resizedTo(newSize);
|
172
|
+
const scaleFactor = Vec2.of(this.region.w / oldWidth, this.region.h / oldHeight);
|
173
|
+
const currentTransfm = Mat33.scaling2D(scaleFactor, this.region.topLeft);
|
174
|
+
this.transform = this.transform.rightMul(currentTransfm);
|
175
|
+
this.previewTransformCmds();
|
176
|
+
}
|
177
|
+
}
|
178
|
+
handleRotateCircleDrag(offset) {
|
179
|
+
this.boxRotation = this.boxRotation % (2 * Math.PI);
|
180
|
+
if (this.boxRotation < 0) {
|
181
|
+
this.boxRotation += 2 * Math.PI;
|
182
|
+
}
|
183
|
+
let targetRotation = offset.angle();
|
184
|
+
targetRotation = targetRotation % (2 * Math.PI);
|
185
|
+
if (targetRotation < 0) {
|
186
|
+
targetRotation += 2 * Math.PI;
|
187
|
+
}
|
188
|
+
let deltaRotation = (targetRotation - this.boxRotation);
|
189
|
+
const rotationStep = Math.PI / 12;
|
190
|
+
if (Math.abs(deltaRotation) < rotationStep || !isFinite(deltaRotation)) {
|
191
|
+
return;
|
192
|
+
}
|
193
|
+
else {
|
194
|
+
const rotationDirection = Math.sign(deltaRotation);
|
195
|
+
// Step exactly one rotationStep
|
196
|
+
deltaRotation = Math.floor(Math.abs(deltaRotation) / rotationStep) * rotationStep;
|
197
|
+
deltaRotation *= rotationDirection;
|
198
|
+
}
|
199
|
+
this.transform = this.transform.rightMul(Mat33.zRotation(deltaRotation, this.region.center));
|
200
|
+
this.boxRotation += deltaRotation;
|
201
|
+
this.previewTransformCmds();
|
202
|
+
}
|
203
|
+
computeTransformCommands() {
|
204
|
+
return this.selectedElems.map(elem => {
|
205
|
+
return elem.transformBy(this.transform);
|
206
|
+
});
|
207
|
+
}
|
208
|
+
// Applies the current transformation to the selection
|
209
|
+
finishDragging() {
|
210
|
+
this.transformationCommands.forEach(cmd => {
|
211
|
+
cmd.unapply(this.editor);
|
212
|
+
});
|
213
|
+
const fullTransform = this.transform;
|
214
|
+
const inverseTransform = this.transform.inverse();
|
215
|
+
const deltaBoxRotation = this.boxRotation;
|
216
|
+
const currentTransfmCommands = this.computeTransformCommands();
|
217
|
+
// Reset for the next drag
|
218
|
+
this.transformationCommands = [];
|
219
|
+
this.transform = Mat33.identity;
|
220
|
+
this.region = this.region.transformedBoundingBox(inverseTransform);
|
221
|
+
// Make the commands undo-able
|
222
|
+
this.editor.dispatch({
|
223
|
+
apply: (editor) => __awaiter(this, void 0, void 0, function* () {
|
224
|
+
// Approximate the new selection
|
225
|
+
this.region = this.region.transformedBoundingBox(fullTransform);
|
226
|
+
this.boxRotation += deltaBoxRotation;
|
227
|
+
this.updateUI();
|
228
|
+
yield editor.asyncApplyCommands(currentTransfmCommands, updateChunkSize);
|
229
|
+
this.recomputeRegion();
|
230
|
+
this.updateUI();
|
231
|
+
}),
|
232
|
+
unapply: (editor) => __awaiter(this, void 0, void 0, function* () {
|
233
|
+
this.region = this.region.transformedBoundingBox(inverseTransform);
|
234
|
+
this.boxRotation -= deltaBoxRotation;
|
235
|
+
this.updateUI();
|
236
|
+
yield editor.asyncUnapplyCommands(currentTransfmCommands, updateChunkSize);
|
237
|
+
this.recomputeRegion();
|
238
|
+
this.updateUI();
|
239
|
+
}),
|
240
|
+
description(localizationTable) {
|
241
|
+
return localizationTable.transformedElements(currentTransfmCommands.length);
|
242
|
+
},
|
243
|
+
});
|
244
|
+
}
|
245
|
+
// Preview the effects of the current transformation on the selection
|
246
|
+
previewTransformCmds() {
|
247
|
+
// Don't render what we're moving if it's likely to be slow.
|
248
|
+
if (this.selectedElems.length > updateChunkSize) {
|
249
|
+
this.updateUI();
|
250
|
+
return;
|
251
|
+
}
|
252
|
+
this.transformationCommands.forEach(cmd => cmd.unapply(this.editor));
|
253
|
+
this.transformationCommands = this.computeTransformCommands();
|
254
|
+
this.transformationCommands.forEach(cmd => cmd.apply(this.editor));
|
255
|
+
this.updateUI();
|
256
|
+
}
|
257
|
+
appendBackgroundBoxTo(elem) {
|
258
|
+
if (this.backgroundBox.parentElement) {
|
259
|
+
this.backgroundBox.remove();
|
260
|
+
}
|
261
|
+
elem.appendChild(this.backgroundBox);
|
262
|
+
}
|
263
|
+
setToPoint(point) {
|
264
|
+
this.region = this.region.grownToPoint(point);
|
265
|
+
this.recomputeBoxRotation();
|
266
|
+
this.updateUI();
|
267
|
+
}
|
268
|
+
cancelSelection() {
|
269
|
+
if (this.backgroundBox.parentElement) {
|
270
|
+
this.backgroundBox.remove();
|
271
|
+
}
|
272
|
+
this.region = Rect2.empty;
|
273
|
+
}
|
274
|
+
// Find the objects corresponding to this in the document,
|
275
|
+
// select them.
|
276
|
+
// Returns false iff nothing was selected.
|
277
|
+
resolveToObjects() {
|
278
|
+
// Grow the rectangle, if necessary
|
279
|
+
if (this.region.w === 0 || this.region.h === 0) {
|
280
|
+
const padding = this.editor.viewport.visibleRect.maxDimension / 100;
|
281
|
+
this.region = Rect2.bboxOf(this.region.corners, padding);
|
282
|
+
}
|
283
|
+
this.selectedElems = this.editor.image.getElementsIntersectingRegion(this.region).filter(elem => {
|
284
|
+
if (this.region.containsRect(elem.getBBox())) {
|
285
|
+
return true;
|
286
|
+
}
|
287
|
+
else if (this.region.getEdges().some(edge => elem.intersects(edge))) {
|
288
|
+
return true;
|
289
|
+
}
|
290
|
+
return false;
|
291
|
+
});
|
292
|
+
// Find the bounding box of all selected elements.
|
293
|
+
if (!this.recomputeRegion()) {
|
294
|
+
return false;
|
295
|
+
}
|
296
|
+
this.updateUI();
|
297
|
+
return true;
|
298
|
+
}
|
299
|
+
// Recompute this' region from the selected elements. Resets rotation to zero.
|
300
|
+
// Returns false if the selection is empty.
|
301
|
+
recomputeRegion() {
|
302
|
+
const newRegion = this.selectedElems.reduce((accumulator, elem) => {
|
303
|
+
return (accumulator !== null && accumulator !== void 0 ? accumulator : elem.getBBox()).union(elem.getBBox());
|
304
|
+
}, null);
|
305
|
+
if (!newRegion) {
|
306
|
+
this.cancelSelection();
|
307
|
+
return false;
|
308
|
+
}
|
309
|
+
this.region = newRegion;
|
310
|
+
const minSize = this.getMinCanvasSize();
|
311
|
+
if (this.region.w < minSize || this.region.h < minSize) {
|
312
|
+
// Add padding
|
313
|
+
const padding = minSize / 2;
|
314
|
+
this.region = Rect2.bboxOf(this.region.corners, padding);
|
315
|
+
}
|
316
|
+
this.recomputeBoxRotation();
|
317
|
+
return true;
|
318
|
+
}
|
319
|
+
getMinCanvasSize() {
|
320
|
+
const canvasHandleSize = handleScreenSize / this.editor.viewport.getScaleFactor();
|
321
|
+
return canvasHandleSize * 2;
|
322
|
+
}
|
323
|
+
recomputeBoxRotation() {
|
324
|
+
this.boxRotation = this.editor.viewport.getRotationAngle();
|
325
|
+
}
|
326
|
+
getSelectedItemCount() {
|
327
|
+
return this.selectedElems.length;
|
328
|
+
}
|
329
|
+
updateUI() {
|
330
|
+
if (!this.backgroundBox) {
|
331
|
+
return;
|
332
|
+
}
|
333
|
+
const rightSideDirection = this.region.topRight.minus(this.region.bottomRight);
|
334
|
+
const topSideDirection = this.region.topLeft.minus(this.region.topRight);
|
335
|
+
const toScreen = this.editor.viewport.canvasToScreenTransform;
|
336
|
+
const centerOnScreen = toScreen.transformVec2(this.region.center);
|
337
|
+
const heightOnScreen = toScreen.transformVec3(rightSideDirection).magnitude();
|
338
|
+
const widthOnScreen = toScreen.transformVec3(topSideDirection).magnitude();
|
339
|
+
this.backgroundBox.style.marginLeft = `${centerOnScreen.x - widthOnScreen / 2}px`;
|
340
|
+
this.backgroundBox.style.marginTop = `${centerOnScreen.y - heightOnScreen / 2}px`;
|
341
|
+
this.backgroundBox.style.width = `${widthOnScreen}px`;
|
342
|
+
this.backgroundBox.style.height = `${heightOnScreen}px`;
|
343
|
+
const rotationDeg = this.boxRotation * 180 / Math.PI;
|
344
|
+
this.backgroundBox.style.transform = `rotate(${rotationDeg}deg)`;
|
345
|
+
this.rotateCircle.style.transform = `rotate(${-rotationDeg}deg)`;
|
346
|
+
}
|
347
|
+
}
|
348
|
+
export default class SelectionTool extends BaseTool {
|
349
|
+
constructor(editor, description) {
|
350
|
+
super(editor.notifier, description);
|
351
|
+
this.editor = editor;
|
352
|
+
this.kind = ToolType.Selection;
|
353
|
+
this.handleOverlay = document.createElement('div');
|
354
|
+
editor.createHTMLOverlay(this.handleOverlay);
|
355
|
+
editor.addStyleSheet(styles);
|
356
|
+
this.handleOverlay.style.display = 'none';
|
357
|
+
this.handleOverlay.classList.add('handleOverlay');
|
358
|
+
editor.notifier.on(EditorEventType.ViewportChanged, _data => {
|
359
|
+
var _a, _b;
|
360
|
+
(_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.recomputeRegion();
|
361
|
+
(_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.updateUI();
|
362
|
+
});
|
363
|
+
}
|
364
|
+
onPointerDown(event) {
|
365
|
+
if (event.allPointers.length === 1 && event.current.isPrimary) {
|
366
|
+
this.prevSelectionBox = this.selectionBox;
|
367
|
+
this.selectionBox = new Selection(event.current.canvasPos, this.editor);
|
368
|
+
// Remove any previous selection rects
|
369
|
+
this.handleOverlay.replaceChildren();
|
370
|
+
this.selectionBox.appendBackgroundBoxTo(this.handleOverlay);
|
371
|
+
return true;
|
372
|
+
}
|
373
|
+
return false;
|
374
|
+
}
|
375
|
+
onPointerMove(event) {
|
376
|
+
if (!this.selectionBox)
|
377
|
+
return;
|
378
|
+
this.selectionBox.setToPoint(event.current.canvasPos);
|
379
|
+
}
|
380
|
+
onGestureEnd() {
|
381
|
+
if (!this.selectionBox)
|
382
|
+
return;
|
383
|
+
// Expand/shrink the selection rectangle, if applicable
|
384
|
+
const hasSelection = this.selectionBox.resolveToObjects();
|
385
|
+
// Note that the selection has changed
|
386
|
+
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
387
|
+
kind: EditorEventType.ToolUpdated,
|
388
|
+
tool: this,
|
389
|
+
});
|
390
|
+
if (hasSelection) {
|
391
|
+
const visibleRect = this.editor.viewport.visibleRect;
|
392
|
+
const selectionRect = this.selectionBox.region;
|
393
|
+
this.editor.announceForAccessibility(this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount()));
|
394
|
+
// Try to move the selection within the center 2/3rds of the viewport.
|
395
|
+
const targetRect = visibleRect.transformedBoundingBox(Mat33.scaling2D(2 / 3, visibleRect.center));
|
396
|
+
// Ensure that the selection fits within the target
|
397
|
+
if (targetRect.w < selectionRect.w || targetRect.h < selectionRect.h) {
|
398
|
+
const multiplier = Math.max(selectionRect.w / targetRect.w, selectionRect.h / targetRect.h);
|
399
|
+
const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
|
400
|
+
const viewportContentTransform = visibleRectTransform.inverse();
|
401
|
+
(new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
|
402
|
+
}
|
403
|
+
// Ensure that the top left is visible
|
404
|
+
if (!targetRect.containsRect(selectionRect)) {
|
405
|
+
// target position - current position
|
406
|
+
const translation = selectionRect.center.minus(targetRect.center);
|
407
|
+
const visibleRectTransform = Mat33.translation(translation);
|
408
|
+
const viewportContentTransform = visibleRectTransform.inverse();
|
409
|
+
(new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
|
410
|
+
}
|
411
|
+
}
|
412
|
+
}
|
413
|
+
onPointerUp(event) {
|
414
|
+
if (!this.selectionBox)
|
415
|
+
return;
|
416
|
+
this.selectionBox.setToPoint(event.current.canvasPos);
|
417
|
+
this.onGestureEnd();
|
418
|
+
}
|
419
|
+
onGestureCancel() {
|
420
|
+
var _a, _b;
|
421
|
+
// Revert to the previous selection, if any.
|
422
|
+
(_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.cancelSelection();
|
423
|
+
this.selectionBox = this.prevSelectionBox;
|
424
|
+
(_b = this.selectionBox) === null || _b === void 0 ? void 0 : _b.appendBackgroundBoxTo(this.handleOverlay);
|
425
|
+
}
|
426
|
+
setEnabled(enabled) {
|
427
|
+
super.setEnabled(enabled);
|
428
|
+
// Clear the selection
|
429
|
+
this.handleOverlay.replaceChildren();
|
430
|
+
this.selectionBox = null;
|
431
|
+
this.handleOverlay.style.display = enabled ? 'block' : 'none';
|
432
|
+
}
|
433
|
+
// Get the object responsible for displaying this' selection.
|
434
|
+
getSelection() {
|
435
|
+
return this.selectionBox;
|
436
|
+
}
|
437
|
+
}
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import { InputEvt } from '../types';
|
2
|
+
import Editor from '../Editor';
|
3
|
+
import BaseTool from './BaseTool';
|
4
|
+
import { ToolLocalization } from './localization';
|
5
|
+
export declare enum ToolType {
|
6
|
+
TouchPanZoom = 0,
|
7
|
+
Pen = 1,
|
8
|
+
Selection = 2,
|
9
|
+
Eraser = 3,
|
10
|
+
PanZoom = 4
|
11
|
+
}
|
12
|
+
export default class ToolController {
|
13
|
+
private tools;
|
14
|
+
private activeTool;
|
15
|
+
constructor(editor: Editor, localization: ToolLocalization);
|
16
|
+
dispatchInputEvent(event: InputEvt): boolean;
|
17
|
+
getMatchingTools(kind: ToolType): BaseTool[];
|
18
|
+
}
|
@@ -0,0 +1,110 @@
|
|
1
|
+
import { InputEvtType, EditorEventType } from '../types';
|
2
|
+
import PanZoom, { PanZoomMode } from './PanZoom';
|
3
|
+
import Pen from './Pen';
|
4
|
+
import ToolEnabledGroup from './ToolEnabledGroup';
|
5
|
+
import Eraser from './Eraser';
|
6
|
+
import SelectionTool from './SelectionTool';
|
7
|
+
import Color4 from '../Color4';
|
8
|
+
export var ToolType;
|
9
|
+
(function (ToolType) {
|
10
|
+
ToolType[ToolType["TouchPanZoom"] = 0] = "TouchPanZoom";
|
11
|
+
ToolType[ToolType["Pen"] = 1] = "Pen";
|
12
|
+
ToolType[ToolType["Selection"] = 2] = "Selection";
|
13
|
+
ToolType[ToolType["Eraser"] = 3] = "Eraser";
|
14
|
+
ToolType[ToolType["PanZoom"] = 4] = "PanZoom";
|
15
|
+
})(ToolType || (ToolType = {}));
|
16
|
+
export default class ToolController {
|
17
|
+
constructor(editor, localization) {
|
18
|
+
const primaryToolEnabledGroup = new ToolEnabledGroup();
|
19
|
+
const touchPanZoom = new PanZoom(editor, PanZoomMode.OneFingerGestures, localization.touchPanTool);
|
20
|
+
const primaryPenTool = new Pen(editor, localization.penTool(1));
|
21
|
+
const primaryTools = [
|
22
|
+
new SelectionTool(editor, localization.selectionTool),
|
23
|
+
new Eraser(editor, localization.eraserTool),
|
24
|
+
// Three pens
|
25
|
+
primaryPenTool,
|
26
|
+
new Pen(editor, localization.penTool(2), Color4.clay, 8),
|
27
|
+
// Highlighter-like pen with width=64
|
28
|
+
new Pen(editor, localization.penTool(3), Color4.ofRGBA(1, 1, 0, 0.5), 64),
|
29
|
+
];
|
30
|
+
this.tools = [
|
31
|
+
touchPanZoom,
|
32
|
+
...primaryTools,
|
33
|
+
new PanZoom(editor, PanZoomMode.TwoFingerGestures | PanZoomMode.AnyDevice, localization.twoFingerPanZoomTool),
|
34
|
+
];
|
35
|
+
primaryTools.forEach(tool => tool.setToolGroup(primaryToolEnabledGroup));
|
36
|
+
touchPanZoom.setEnabled(false);
|
37
|
+
primaryPenTool.setEnabled(true);
|
38
|
+
editor.notifier.on(EditorEventType.ToolEnabled, event => {
|
39
|
+
if (event.kind === EditorEventType.ToolEnabled) {
|
40
|
+
editor.announceForAccessibility(localization.toolEnabledAnnouncement(event.tool.description));
|
41
|
+
}
|
42
|
+
});
|
43
|
+
editor.notifier.on(EditorEventType.ToolDisabled, event => {
|
44
|
+
if (event.kind === EditorEventType.ToolDisabled) {
|
45
|
+
editor.announceForAccessibility(localization.toolDisabledAnnouncement(event.tool.description));
|
46
|
+
}
|
47
|
+
});
|
48
|
+
this.activeTool = null;
|
49
|
+
}
|
50
|
+
// Returns true if the event was handled
|
51
|
+
dispatchInputEvent(event) {
|
52
|
+
var _a, _b;
|
53
|
+
let handled = false;
|
54
|
+
if (event.kind === InputEvtType.PointerDownEvt) {
|
55
|
+
for (const tool of this.tools) {
|
56
|
+
if (tool.isEnabled() && tool.onPointerDown(event)) {
|
57
|
+
if (this.activeTool !== tool) {
|
58
|
+
(_a = this.activeTool) === null || _a === void 0 ? void 0 : _a.onGestureCancel();
|
59
|
+
}
|
60
|
+
this.activeTool = tool;
|
61
|
+
handled = true;
|
62
|
+
break;
|
63
|
+
}
|
64
|
+
}
|
65
|
+
}
|
66
|
+
else if (event.kind === InputEvtType.PointerUpEvt) {
|
67
|
+
(_b = this.activeTool) === null || _b === void 0 ? void 0 : _b.onPointerUp(event);
|
68
|
+
this.activeTool = null;
|
69
|
+
handled = true;
|
70
|
+
}
|
71
|
+
else if (event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent) {
|
72
|
+
const isKeyPressEvt = event.kind === InputEvtType.KeyPressEvent;
|
73
|
+
const isWheelEvt = event.kind === InputEvtType.WheelEvt;
|
74
|
+
for (const tool of this.tools) {
|
75
|
+
if (!tool.isEnabled()) {
|
76
|
+
continue;
|
77
|
+
}
|
78
|
+
const wheelResult = isWheelEvt && tool.onWheel(event);
|
79
|
+
const keyPressResult = isKeyPressEvt && tool.onKeyPress(event);
|
80
|
+
handled = keyPressResult || wheelResult;
|
81
|
+
if (handled) {
|
82
|
+
break;
|
83
|
+
}
|
84
|
+
}
|
85
|
+
}
|
86
|
+
else if (this.activeTool !== null) {
|
87
|
+
let allCasesHandledGuard;
|
88
|
+
switch (event.kind) {
|
89
|
+
case InputEvtType.PointerMoveEvt:
|
90
|
+
this.activeTool.onPointerMove(event);
|
91
|
+
break;
|
92
|
+
case InputEvtType.GestureCancelEvt:
|
93
|
+
this.activeTool.onGestureCancel();
|
94
|
+
this.activeTool = null;
|
95
|
+
break;
|
96
|
+
default:
|
97
|
+
allCasesHandledGuard = event;
|
98
|
+
return allCasesHandledGuard;
|
99
|
+
}
|
100
|
+
handled = true;
|
101
|
+
}
|
102
|
+
else {
|
103
|
+
handled = false;
|
104
|
+
}
|
105
|
+
return handled;
|
106
|
+
}
|
107
|
+
getMatchingTools(kind) {
|
108
|
+
return this.tools.filter(tool => tool.kind === kind);
|
109
|
+
}
|
110
|
+
}
|
@@ -0,0 +1,11 @@
|
|
1
|
+
// Connects a group of tools -- at most one tool in the group must be enabled.
|
2
|
+
export default class ToolEnabledGroup {
|
3
|
+
constructor() { }
|
4
|
+
notifyEnabled(tool) {
|
5
|
+
var _a;
|
6
|
+
if (tool !== this.activeTool) {
|
7
|
+
(_a = this.activeTool) === null || _a === void 0 ? void 0 : _a.setEnabled(false);
|
8
|
+
this.activeTool = tool;
|
9
|
+
}
|
10
|
+
}
|
11
|
+
}
|
@@ -0,0 +1,10 @@
|
|
1
|
+
export interface ToolLocalization {
|
2
|
+
penTool: (penId: number) => string;
|
3
|
+
selectionTool: string;
|
4
|
+
eraserTool: string;
|
5
|
+
touchPanTool: string;
|
6
|
+
twoFingerPanZoomTool: string;
|
7
|
+
toolEnabledAnnouncement: (toolName: string) => string;
|
8
|
+
toolDisabledAnnouncement: (toolName: string) => string;
|
9
|
+
}
|
10
|
+
export declare const defaultToolLocalization: ToolLocalization;
|
@@ -0,0 +1,9 @@
|
|
1
|
+
export const defaultToolLocalization = {
|
2
|
+
penTool: (penId) => `Pen ${penId}`,
|
3
|
+
selectionTool: 'Selection',
|
4
|
+
eraserTool: 'Eraser',
|
5
|
+
touchPanTool: 'Touch Panning',
|
6
|
+
twoFingerPanZoomTool: 'Panning and Zooming',
|
7
|
+
toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
|
8
|
+
toolDisabledAnnouncement: (toolName) => `${toolName} disabled`,
|
9
|
+
};
|