js-draw 0.3.2 → 0.4.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/.github/pull_request_template.md +15 -0
  2. package/.github/workflows/firebase-hosting-merge.yml +7 -0
  3. package/.github/workflows/firebase-hosting-pull-request.yml +10 -0
  4. package/.github/workflows/github-pages.yml +2 -0
  5. package/CHANGELOG.md +16 -1
  6. package/README.md +1 -3
  7. package/dist/bundle.js +1 -1
  8. package/dist/src/Editor.d.ts +11 -0
  9. package/dist/src/Editor.js +107 -77
  10. package/dist/src/Pointer.d.ts +1 -1
  11. package/dist/src/Pointer.js +8 -3
  12. package/dist/src/Viewport.d.ts +1 -0
  13. package/dist/src/Viewport.js +14 -1
  14. package/dist/src/components/AbstractComponent.js +1 -0
  15. package/dist/src/components/ImageComponent.d.ts +2 -2
  16. package/dist/src/components/Stroke.js +15 -9
  17. package/dist/src/components/Text.d.ts +1 -1
  18. package/dist/src/components/Text.js +1 -1
  19. package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -0
  20. package/dist/src/components/builders/FreehandLineBuilder.js +34 -36
  21. package/dist/src/language/assertions.d.ts +1 -0
  22. package/dist/src/language/assertions.js +5 -0
  23. package/dist/src/math/Mat33.d.ts +38 -2
  24. package/dist/src/math/Mat33.js +30 -1
  25. package/dist/src/math/Path.d.ts +1 -1
  26. package/dist/src/math/Path.js +10 -8
  27. package/dist/src/math/Vec3.d.ts +12 -2
  28. package/dist/src/math/Vec3.js +16 -1
  29. package/dist/src/math/rounding.d.ts +1 -0
  30. package/dist/src/math/rounding.js +13 -6
  31. package/dist/src/rendering/renderers/AbstractRenderer.js +2 -1
  32. package/dist/src/testing/beforeEachFile.d.ts +1 -0
  33. package/dist/src/testing/beforeEachFile.js +3 -0
  34. package/dist/src/testing/createEditor.d.ts +1 -0
  35. package/dist/src/testing/createEditor.js +7 -1
  36. package/dist/src/testing/loadExpectExtensions.d.ts +0 -15
  37. package/dist/src/toolbar/HTMLToolbar.js +5 -4
  38. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
  39. package/dist/src/tools/PasteHandler.js +3 -1
  40. package/dist/src/tools/Pen.js +1 -1
  41. package/dist/src/tools/SelectionTool/Selection.d.ts +54 -0
  42. package/dist/src/tools/SelectionTool/Selection.js +337 -0
  43. package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +35 -0
  44. package/dist/src/tools/SelectionTool/SelectionHandle.js +75 -0
  45. package/dist/src/tools/SelectionTool/SelectionTool.d.ts +31 -0
  46. package/dist/src/tools/SelectionTool/SelectionTool.js +284 -0
  47. package/dist/src/tools/SelectionTool/TransformMode.d.ts +34 -0
  48. package/dist/src/tools/SelectionTool/TransformMode.js +98 -0
  49. package/dist/src/tools/SelectionTool/types.d.ts +9 -0
  50. package/dist/src/tools/SelectionTool/types.js +11 -0
  51. package/dist/src/tools/ToolController.js +1 -1
  52. package/dist/src/tools/lib.d.ts +1 -1
  53. package/dist/src/tools/lib.js +1 -1
  54. package/dist/src/types.d.ts +1 -1
  55. package/jest.config.js +5 -0
  56. package/package.json +15 -14
  57. package/src/Editor.css +1 -0
  58. package/src/Editor.ts +147 -108
  59. package/src/Pointer.ts +8 -3
  60. package/src/Viewport.ts +17 -2
  61. package/src/components/AbstractComponent.ts +4 -6
  62. package/src/components/ImageComponent.ts +2 -6
  63. package/src/components/Stroke.test.ts +0 -3
  64. package/src/components/Stroke.ts +14 -7
  65. package/src/components/Text.test.ts +0 -3
  66. package/src/components/Text.ts +4 -8
  67. package/src/components/builders/FreehandLineBuilder.ts +37 -43
  68. package/src/language/assertions.ts +6 -0
  69. package/src/math/LineSegment2.test.ts +8 -10
  70. package/src/math/Mat33.test.ts +14 -2
  71. package/src/math/Mat33.ts +43 -2
  72. package/src/math/Path.toString.test.ts +12 -1
  73. package/src/math/Path.ts +11 -9
  74. package/src/math/Rect2.test.ts +0 -3
  75. package/src/math/Vec2.test.ts +0 -3
  76. package/src/math/Vec3.test.ts +0 -3
  77. package/src/math/Vec3.ts +23 -2
  78. package/src/math/rounding.test.ts +30 -5
  79. package/src/math/rounding.ts +16 -7
  80. package/src/rendering/renderers/AbstractRenderer.ts +3 -2
  81. package/src/testing/beforeEachFile.ts +3 -0
  82. package/src/testing/createEditor.ts +8 -1
  83. package/src/testing/global.d.ts +17 -0
  84. package/src/testing/loadExpectExtensions.ts +0 -15
  85. package/src/toolbar/HTMLToolbar.ts +5 -4
  86. package/src/toolbar/toolbar.css +3 -2
  87. package/src/toolbar/widgets/SelectionToolWidget.ts +1 -1
  88. package/src/tools/PasteHandler.ts +4 -1
  89. package/src/tools/Pen.test.ts +150 -0
  90. package/src/tools/Pen.ts +1 -1
  91. package/src/tools/SelectionTool/Selection.ts +455 -0
  92. package/src/tools/SelectionTool/SelectionHandle.ts +99 -0
  93. package/src/tools/SelectionTool/SelectionTool.css +22 -0
  94. package/src/tools/{SelectionTool.test.ts → SelectionTool/SelectionTool.test.ts} +21 -21
  95. package/src/tools/SelectionTool/SelectionTool.ts +344 -0
  96. package/src/tools/SelectionTool/TransformMode.ts +114 -0
  97. package/src/tools/SelectionTool/types.ts +11 -0
  98. package/src/tools/ToolController.ts +1 -1
  99. package/src/tools/lib.ts +1 -1
  100. package/src/types.ts +1 -1
  101. package/tsconfig.json +3 -1
  102. package/dist/src/tools/SelectionTool.d.ts +0 -65
  103. package/dist/src/tools/SelectionTool.js +0 -647
  104. package/src/tools/SelectionTool.ts +0 -797
@@ -1,797 +0,0 @@
1
- // Allows users to select/transform portions of the `EditorImage`.
2
- // With respect to `extend`ing, `SelectionTool` is not stable.
3
- // @packageDocumentation
4
-
5
- import Command from '../commands/Command';
6
- import Duplicate from '../commands/Duplicate';
7
- import Erase from '../commands/Erase';
8
- import AbstractComponent from '../components/AbstractComponent';
9
- import Editor from '../Editor';
10
- import Mat33 from '../math/Mat33';
11
- import Rect2 from '../math/Rect2';
12
- import { Point2, Vec2 } from '../math/Vec2';
13
- import { EditorLocalization } from '../localization';
14
- import { CopyEvent, EditorEventType, KeyPressEvent, KeyUpEvent, PointerEvt } from '../types';
15
- import Viewport from '../Viewport';
16
- import BaseTool from './BaseTool';
17
- import SerializableCommand from '../commands/SerializableCommand';
18
- import SVGRenderer from '../rendering/renderers/SVGRenderer';
19
-
20
- const handleScreenSize = 30;
21
- const styles = `
22
- .handleOverlay {
23
- }
24
-
25
- .handleOverlay > .selectionBox {
26
- position: absolute;
27
- z-index: 0;
28
- transform-origin: center;
29
- }
30
-
31
- .handleOverlay > .selectionBox .draggableBackground {
32
- position: absolute;
33
- top: 0;
34
- left: 0;
35
- right: 0;
36
- bottom: 0;
37
-
38
- background-color: var(--secondary-background-color);
39
- opacity: 0.8;
40
- border: 1px solid var(--primary-background-color);
41
- }
42
-
43
- .handleOverlay .resizeCorner {
44
- width: ${handleScreenSize}px;
45
- height: ${handleScreenSize}px;
46
- margin-right: -${handleScreenSize / 2}px;
47
- margin-bottom: -${handleScreenSize / 2}px;
48
-
49
- position: absolute;
50
- bottom: 0;
51
- right: 0;
52
-
53
- opacity: 0.8;
54
- background-color: var(--primary-background-color);
55
- border: 1px solid var(--primary-foreground-color);
56
- }
57
-
58
- .handleOverlay > .selectionBox .rotateCircleContainer {
59
- position: absolute;
60
- top: 50%;
61
- bottom: 50%;
62
- left: 50%;
63
- right: 50%;
64
- }
65
-
66
- .handleOverlay .rotateCircle {
67
- width: ${handleScreenSize}px;
68
- height: ${handleScreenSize}px;
69
- margin-left: -${handleScreenSize / 2}px;
70
- margin-top: -${handleScreenSize / 2}px;
71
- opacity: 0.8;
72
-
73
- border: 1px solid var(--primary-foreground-color);
74
- background-color: var(--primary-background-color);
75
- border-radius: 100%;
76
- }
77
- `;
78
-
79
- type DragCallback = (delta: Vec2, offset: Point2)=> void;
80
- type DragEndCallback = ()=> void;
81
-
82
- const makeDraggable = (element: HTMLElement, onDrag: DragCallback, onDragEnd: DragEndCallback) => {
83
- element.style.touchAction = 'none';
84
- let down = false;
85
-
86
- // Work around a Safari bug
87
- element.addEventListener('touchstart', evt => evt.preventDefault());
88
-
89
- let lastX: number;
90
- let lastY: number;
91
- element.addEventListener('pointerdown', event => {
92
- if (event.isPrimary) {
93
- down = true;
94
- element.setPointerCapture(event.pointerId);
95
- lastX = event.pageX;
96
- lastY = event.pageY;
97
-
98
- return true;
99
- }
100
- return false;
101
- });
102
- element.addEventListener('pointermove', event => {
103
- if (event.isPrimary && down) {
104
- // Safari/iOS doesn't seem to support movementX/movementY on pointer events.
105
- // Calculate manually:
106
- const delta = Vec2.of(event.pageX - lastX, event.pageY - lastY);
107
- onDrag(delta, Vec2.of(event.offsetX, event.offsetY));
108
- lastX = event.pageX;
109
- lastY = event.pageY;
110
-
111
- return true;
112
- }
113
- return false;
114
- });
115
- const onPointerEnd = (event: PointerEvent) => {
116
- if (event.isPrimary) {
117
- down = false;
118
- onDragEnd();
119
-
120
- return true;
121
- }
122
- return false;
123
- };
124
- element.addEventListener('pointerup', onPointerEnd);
125
- element.addEventListener('pointercancel', onPointerEnd);
126
- };
127
-
128
- // Maximum number of strokes to transform without a re-render.
129
- const updateChunkSize = 100;
130
-
131
- // @internal
132
- class Selection {
133
- public region: Rect2;
134
- private boxRotation: number;
135
- private backgroundBox: HTMLElement;
136
- private rotateCircle: HTMLElement;
137
- private selectedElems: AbstractComponent[];
138
- private transform: Mat33;
139
- private transformationCommands: SerializableCommand[];
140
-
141
- public constructor(
142
- public startPoint: Point2, private editor: Editor
143
- ) {
144
- this.boxRotation = this.editor.viewport.getRotationAngle();
145
- this.selectedElems = [];
146
- this.region = Rect2.bboxOf([startPoint]);
147
-
148
- // Create draggable rectangles
149
- this.backgroundBox = document.createElement('div');
150
- const draggableBackground = document.createElement('div');
151
- const resizeCorner = document.createElement('div');
152
- this.rotateCircle = document.createElement('div');
153
- const rotateCircleContainer = document.createElement('div');
154
-
155
- this.backgroundBox.classList.add('selectionBox');
156
- draggableBackground.classList.add('draggableBackground');
157
- resizeCorner.classList.add('resizeCorner');
158
- this.rotateCircle.classList.add('rotateCircle');
159
- rotateCircleContainer.classList.add('rotateCircleContainer');
160
-
161
- rotateCircleContainer.appendChild(this.rotateCircle);
162
-
163
- this.backgroundBox.appendChild(draggableBackground);
164
- this.backgroundBox.appendChild(rotateCircleContainer);
165
- this.backgroundBox.appendChild(resizeCorner);
166
-
167
- this.transformationCommands = [];
168
- this.transform = Mat33.identity;
169
-
170
- makeDraggable(draggableBackground, (deltaPosition: Vec2) => {
171
- this.handleBackgroundDrag(deltaPosition);
172
- }, () => this.finalizeTransform());
173
-
174
- makeDraggable(resizeCorner, (deltaPosition) => {
175
- this.handleResizeCornerDrag(deltaPosition);
176
- }, () => this.finalizeTransform());
177
-
178
- makeDraggable(this.rotateCircle, (_deltaPosition, offset) => {
179
- this.handleRotateCircleDrag(offset);
180
- }, () => this.finalizeTransform());
181
- }
182
-
183
- // Note a small change in the position of this' background while dragging
184
- // At the end of a drag, changes should be applied by calling this.finishDragging()
185
- public handleBackgroundDrag(deltaPosition: Vec2) {
186
- // Re-scale the change in position
187
- // (use a Vec3 transform to avoid translating deltaPosition)
188
- deltaPosition = this.editor.viewport.screenToCanvasTransform.transformVec3(
189
- deltaPosition
190
- );
191
-
192
- // Snap position to a multiple of 10 (additional decimal points lead to larger files).
193
- deltaPosition = this.editor.viewport.roundPoint(deltaPosition);
194
-
195
- this.transformPreview(Mat33.translation(deltaPosition));
196
- }
197
-
198
- public handleResizeCornerDrag(deltaPosition: Vec2) {
199
- deltaPosition = this.editor.viewport.screenToCanvasTransform.transformVec3(
200
- deltaPosition
201
- );
202
- deltaPosition = this.editor.viewport.roundPoint(deltaPosition);
203
-
204
- const oldWidth = this.region.w;
205
- const oldHeight = this.region.h;
206
- const newSize = this.region.size.plus(deltaPosition);
207
-
208
- if (newSize.y > 0 && newSize.x > 0) {
209
- const scaleFactor = Vec2.of(newSize.x / oldWidth, newSize.y / oldHeight);
210
-
211
- this.transformPreview(Mat33.scaling2D(scaleFactor, this.region.topLeft));
212
- }
213
- }
214
-
215
- public handleRotateCircleDrag(offset: Vec2) {
216
- let targetRotation = offset.angle();
217
- targetRotation = targetRotation % (2 * Math.PI);
218
- if (targetRotation < 0) {
219
- targetRotation += 2 * Math.PI;
220
- }
221
-
222
- let deltaRotation = (targetRotation - this.boxRotation);
223
-
224
- const rotationStep = Math.PI / 12;
225
- if (Math.abs(deltaRotation) < rotationStep || !isFinite(deltaRotation)) {
226
- return;
227
- } else {
228
- const rotationDirection = Math.sign(deltaRotation);
229
-
230
- // Step exactly one rotationStep
231
- deltaRotation = Math.floor(Math.abs(deltaRotation) / rotationStep) * rotationStep;
232
- deltaRotation *= rotationDirection;
233
- }
234
-
235
- this.transformPreview(Mat33.zRotation(deltaRotation, this.region.center));
236
- }
237
-
238
- private computeTransformCommands(): SerializableCommand[] {
239
- return this.selectedElems.map(elem => {
240
- return elem.transformBy(this.transform);
241
- });
242
- }
243
-
244
- // Applies, previews, but doesn't finalize the given transformation.
245
- public transformPreview(transform: Mat33) {
246
- this.transform = this.transform.rightMul(transform);
247
- const deltaRotation = transform.transformVec3(Vec2.unitX).angle();
248
- transform = transform.rightMul(Mat33.zRotation(-deltaRotation, this.region.center));
249
-
250
- this.boxRotation += deltaRotation;
251
- this.boxRotation = this.boxRotation % (2 * Math.PI);
252
- if (this.boxRotation < 0) {
253
- this.boxRotation += 2 * Math.PI;
254
- }
255
-
256
- const newSize = transform.transformVec3(this.region.size);
257
- const translation = transform.transformVec2(this.region.topLeft).minus(this.region.topLeft);
258
- this.region = this.region.resizedTo(newSize);
259
- this.region = this.region.translatedBy(translation);
260
-
261
- this.previewTransformCmds();
262
- this.scrollTo();
263
- }
264
-
265
- // Applies the current transformation to the selection
266
- public finalizeTransform() {
267
- this.transformationCommands.forEach(cmd => {
268
- cmd.unapply(this.editor);
269
- });
270
-
271
- const fullTransform = this.transform;
272
- const inverseTransform = this.transform.inverse();
273
- const deltaBoxRotation = this.boxRotation;
274
- const currentTransfmCommands = this.computeTransformCommands();
275
-
276
- // Reset for the next drag
277
- this.transformationCommands = [];
278
- this.transform = Mat33.identity;
279
- this.region = this.region.transformedBoundingBox(inverseTransform);
280
-
281
- // Make the commands undo-able
282
- this.editor.dispatch(new Selection.ApplyTransformationCommand(
283
- this, currentTransfmCommands, fullTransform, deltaBoxRotation
284
- ));
285
- }
286
-
287
- static {
288
- SerializableCommand.register('selection-tool-transform', (json: any, editor) => {
289
- // The selection box is lost when serializing/deserializing. No need to store box rotation
290
- const guiBoxRotation = 0;
291
- const fullTransform: Mat33 = new Mat33(...(json.transform as [
292
- number, number, number,
293
- number, number, number,
294
- number, number, number,
295
- ]));
296
- const commands = (json.commands as any[]).map(data => SerializableCommand.deserialize(data, editor));
297
-
298
- return new this.ApplyTransformationCommand(null, commands, fullTransform, guiBoxRotation);
299
- });
300
- }
301
-
302
- private static ApplyTransformationCommand = class extends SerializableCommand {
303
- public constructor(
304
- private selection: Selection|null,
305
- private currentTransfmCommands: SerializableCommand[],
306
- private fullTransform: Mat33,
307
- private deltaBoxRotation: number,
308
- ) {
309
- super('selection-tool-transform');
310
- }
311
-
312
- public async apply(editor: Editor) {
313
- // Approximate the new selection
314
- if (this.selection) {
315
- this.selection.region = this.selection.region.transformedBoundingBox(this.fullTransform);
316
- this.selection.boxRotation += this.deltaBoxRotation;
317
- this.selection.updateUI();
318
- }
319
-
320
- await editor.asyncApplyCommands(this.currentTransfmCommands, updateChunkSize);
321
- this.selection?.recomputeRegion();
322
- this.selection?.updateUI();
323
- }
324
-
325
- public async unapply(editor: Editor) {
326
- if (this.selection) {
327
- this.selection.region = this.selection.region.transformedBoundingBox(this.fullTransform.inverse());
328
- this.selection.boxRotation -= this.deltaBoxRotation;
329
- this.selection.updateUI();
330
- }
331
-
332
- await editor.asyncUnapplyCommands(this.currentTransfmCommands, updateChunkSize);
333
- this.selection?.recomputeRegion();
334
- this.selection?.updateUI();
335
- }
336
-
337
- protected serializeToJSON() {
338
- return {
339
- commands: this.currentTransfmCommands.map(command => command.serialize()),
340
- transform: this.fullTransform.toArray(),
341
- };
342
- }
343
-
344
- public description(_editor: Editor, localizationTable: EditorLocalization) {
345
- return localizationTable.transformedElements(this.currentTransfmCommands.length);
346
- }
347
- };
348
-
349
- // Preview the effects of the current transformation on the selection
350
- private previewTransformCmds() {
351
- // Don't render what we're moving if it's likely to be slow.
352
- if (this.selectedElems.length > updateChunkSize) {
353
- this.updateUI();
354
- return;
355
- }
356
-
357
- this.transformationCommands.forEach(cmd => cmd.unapply(this.editor));
358
- this.transformationCommands = this.computeTransformCommands();
359
- this.transformationCommands.forEach(cmd => cmd.apply(this.editor));
360
-
361
- this.updateUI();
362
- }
363
-
364
- public appendBackgroundBoxTo(elem: HTMLElement) {
365
- if (this.backgroundBox.parentElement) {
366
- this.backgroundBox.remove();
367
- }
368
-
369
- elem.appendChild(this.backgroundBox);
370
- }
371
-
372
- public setToPoint(point: Point2) {
373
- this.region = this.region.grownToPoint(point);
374
- this.recomputeBoxRotation();
375
- this.updateUI();
376
- }
377
-
378
- public cancelSelection() {
379
- if (this.backgroundBox.parentElement) {
380
- this.backgroundBox.remove();
381
- }
382
- this.region = Rect2.empty;
383
- }
384
-
385
- public setSelectedObjects(objects: AbstractComponent[], bbox: Rect2) {
386
- this.region = bbox;
387
- this.selectedElems = objects;
388
- this.updateUI();
389
- }
390
-
391
- public getSelectedObjects(): AbstractComponent[] {
392
- return this.selectedElems;
393
- }
394
-
395
- // Find the objects corresponding to this in the document,
396
- // select them.
397
- // Returns false iff nothing was selected.
398
- public resolveToObjects(): boolean {
399
- // Grow the rectangle, if necessary
400
- if (this.region.w === 0 || this.region.h === 0) {
401
- const padding = this.editor.viewport.visibleRect.maxDimension / 100;
402
- this.region = Rect2.bboxOf(this.region.corners, padding);
403
- }
404
-
405
- this.selectedElems = this.editor.image.getElementsIntersectingRegion(this.region).filter(elem => {
406
- if (this.region.containsRect(elem.getBBox())) {
407
- return true;
408
- }
409
-
410
- // Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
411
- // As such, test with more lines than just this' edges.
412
- const testLines = [];
413
- for (const subregion of this.region.divideIntoGrid(2, 2)) {
414
- testLines.push(...subregion.getEdges());
415
- }
416
-
417
- return testLines.some(edge => elem.intersects(edge));
418
- });
419
-
420
- // Find the bounding box of all selected elements.
421
- if (!this.recomputeRegion()) {
422
- return false;
423
- }
424
- this.updateUI();
425
-
426
- return true;
427
- }
428
-
429
- // Recompute this' region from the selected elements. Resets rotation to zero.
430
- // Returns false if the selection is empty.
431
- public recomputeRegion(): boolean {
432
- const newRegion = this.selectedElems.reduce((
433
- accumulator: Rect2|null, elem: AbstractComponent
434
- ): Rect2 => {
435
- return (accumulator ?? elem.getBBox()).union(elem.getBBox());
436
- }, null);
437
-
438
- if (!newRegion) {
439
- this.cancelSelection();
440
- return false;
441
- }
442
-
443
- this.region = newRegion;
444
-
445
-
446
- const minSize = this.getMinCanvasSize();
447
- if (this.region.w < minSize || this.region.h < minSize) {
448
- // Add padding
449
- const padding = minSize / 2;
450
- this.region = Rect2.bboxOf(
451
- this.region.corners, padding
452
- );
453
- }
454
-
455
- this.recomputeBoxRotation();
456
- return true;
457
- }
458
-
459
- public getMinCanvasSize(): number {
460
- const canvasHandleSize = handleScreenSize / this.editor.viewport.getScaleFactor();
461
- return canvasHandleSize * 2;
462
- }
463
-
464
- private recomputeBoxRotation() {
465
- this.boxRotation = this.editor.viewport.getRotationAngle();
466
- }
467
-
468
- public getSelectedItemCount() {
469
- return this.selectedElems.length;
470
- }
471
-
472
- public updateUI() {
473
- if (!this.backgroundBox) {
474
- return;
475
- }
476
-
477
- const rightSideDirection = this.region.topRight.minus(this.region.bottomRight);
478
- const topSideDirection = this.region.topLeft.minus(this.region.topRight);
479
-
480
- const toScreen = this.editor.viewport.canvasToScreenTransform;
481
- const centerOnScreen = toScreen.transformVec2(this.region.center);
482
- const heightOnScreen = toScreen.transformVec3(rightSideDirection).magnitude();
483
- const widthOnScreen = toScreen.transformVec3(topSideDirection).magnitude();
484
-
485
- this.backgroundBox.style.marginLeft = `${centerOnScreen.x - widthOnScreen / 2}px`;
486
- this.backgroundBox.style.marginTop = `${centerOnScreen.y - heightOnScreen / 2}px`;
487
- this.backgroundBox.style.width = `${widthOnScreen}px`;
488
- this.backgroundBox.style.height = `${heightOnScreen}px`;
489
-
490
- const rotationDeg = this.boxRotation * 180 / Math.PI;
491
-
492
- this.backgroundBox.style.transform = `rotate(${rotationDeg}deg)`;
493
- this.rotateCircle.style.transform = `rotate(${-rotationDeg}deg)`;
494
- }
495
-
496
- // Scroll the viewport to this. Does not zoom
497
- public scrollTo() {
498
- const viewport = this.editor.viewport;
499
- const visibleRect = viewport.visibleRect;
500
- if (!visibleRect.containsPoint(this.region.center)) {
501
- const closestPoint = visibleRect.getClosestPointOnBoundaryTo(this.region.center);
502
- const delta = this.region.center.minus(closestPoint);
503
- this.editor.dispatchNoAnnounce(
504
- Viewport.transformBy(Mat33.translation(delta.times(-1))), false
505
- );
506
- }
507
- }
508
-
509
- public deleteSelectedObjects(): Command {
510
- return new Erase(this.selectedElems);
511
- }
512
-
513
- public duplicateSelectedObjects(): Command {
514
- return new Duplicate(this.selectedElems);
515
- }
516
- }
517
-
518
- // {@inheritDoc SelectionTool!}
519
- export default class SelectionTool extends BaseTool {
520
- private handleOverlay: HTMLElement;
521
- private prevSelectionBox: Selection|null;
522
- private selectionBox: Selection|null;
523
-
524
- public constructor(private editor: Editor, description: string) {
525
- super(editor.notifier, description);
526
-
527
- this.handleOverlay = document.createElement('div');
528
- editor.createHTMLOverlay(this.handleOverlay);
529
- editor.addStyleSheet(styles);
530
-
531
- this.handleOverlay.style.display = 'none';
532
- this.handleOverlay.classList.add('handleOverlay');
533
-
534
- editor.notifier.on(EditorEventType.ViewportChanged, _data => {
535
- this.selectionBox?.recomputeRegion();
536
- this.selectionBox?.updateUI();
537
- });
538
-
539
- this.editor.handleKeyEventsFrom(this.handleOverlay);
540
- }
541
-
542
- private makeSelectionBox(selectionStartPos: Point2) {
543
- this.prevSelectionBox = this.selectionBox;
544
- this.selectionBox = new Selection(
545
- selectionStartPos, this.editor
546
- );
547
- // Remove any previous selection rects
548
- this.handleOverlay.replaceChildren();
549
- this.selectionBox.appendBackgroundBoxTo(this.handleOverlay);
550
- }
551
-
552
- public onPointerDown(event: PointerEvt): boolean {
553
- if (event.allPointers.length === 1 && event.current.isPrimary) {
554
- this.makeSelectionBox(event.current.canvasPos);
555
-
556
- return true;
557
- }
558
- return false;
559
- }
560
-
561
- public onPointerMove(event: PointerEvt): void {
562
- if (!this.selectionBox) return;
563
-
564
- this.selectionBox!.setToPoint(event.current.canvasPos);
565
- }
566
-
567
- private onGestureEnd() {
568
- if (!this.selectionBox) return;
569
-
570
- // Expand/shrink the selection rectangle, if applicable
571
- const hasSelection = this.selectionBox.resolveToObjects();
572
-
573
- // Note that the selection has changed
574
- this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
575
- kind: EditorEventType.ToolUpdated,
576
- tool: this,
577
- });
578
-
579
- if (hasSelection) {
580
- this.editor.announceForAccessibility(
581
- this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount())
582
- );
583
- this.zoomToSelection();
584
- }
585
- }
586
-
587
- private zoomToSelection() {
588
- if (this.selectionBox) {
589
- const selectionRect = this.selectionBox.region;
590
- this.editor.dispatchNoAnnounce(this.editor.viewport.zoomTo(selectionRect, false), false);
591
- }
592
- }
593
-
594
- public onPointerUp(event: PointerEvt): void {
595
- if (!this.selectionBox) return;
596
-
597
- this.selectionBox.setToPoint(event.current.canvasPos);
598
- this.onGestureEnd();
599
- }
600
-
601
- public onGestureCancel(): void {
602
- // Revert to the previous selection, if any.
603
- this.selectionBox?.cancelSelection();
604
- this.selectionBox = this.prevSelectionBox;
605
- this.selectionBox?.appendBackgroundBoxTo(this.handleOverlay);
606
- }
607
-
608
- private static handleableKeys = [
609
- 'a', 'h', 'ArrowLeft',
610
- 'd', 'l', 'ArrowRight',
611
- 'q', 'k', 'ArrowUp',
612
- 'e', 'j', 'ArrowDown',
613
- 'r', 'R',
614
- 'i', 'I', 'o', 'O',
615
- ];
616
- public onKeyPress(event: KeyPressEvent): boolean {
617
- let rotationSteps = 0;
618
- let xTranslateSteps = 0;
619
- let yTranslateSteps = 0;
620
- let xScaleSteps = 0;
621
- let yScaleSteps = 0;
622
-
623
- switch (event.key) {
624
- case 'a':
625
- case 'h':
626
- case 'ArrowLeft':
627
- xTranslateSteps -= 1;
628
- break;
629
- case 'd':
630
- case 'l':
631
- case 'ArrowRight':
632
- xTranslateSteps += 1;
633
- break;
634
- case 'q':
635
- case 'k':
636
- case 'ArrowUp':
637
- yTranslateSteps -= 1;
638
- break;
639
- case 'e':
640
- case 'j':
641
- case 'ArrowDown':
642
- yTranslateSteps += 1;
643
- break;
644
- case 'r':
645
- rotationSteps += 1;
646
- break;
647
- case 'R':
648
- rotationSteps -= 1;
649
- break;
650
- case 'i':
651
- xScaleSteps -= 1;
652
- break;
653
- case 'I':
654
- xScaleSteps += 1;
655
- break;
656
- case 'o':
657
- yScaleSteps -= 1;
658
- break;
659
- case 'O':
660
- yScaleSteps += 1;
661
- break;
662
- }
663
-
664
- let handled = xTranslateSteps !== 0
665
- || yTranslateSteps !== 0
666
- || rotationSteps !== 0
667
- || xScaleSteps !== 0
668
- || yScaleSteps !== 0;
669
-
670
- if (!this.selectionBox) {
671
- handled = false;
672
- } else if (handled) {
673
- const translateStepSize = 10 * this.editor.viewport.getSizeOfPixelOnCanvas();
674
- const rotateStepSize = Math.PI / 8;
675
- const scaleStepSize = translateStepSize / 2;
676
-
677
- const region = this.selectionBox.region;
678
- const scaledSize = this.selectionBox.region.size.plus(
679
- Vec2.of(xScaleSteps, yScaleSteps).times(scaleStepSize)
680
- );
681
-
682
- const transform = Mat33.scaling2D(
683
- Vec2.of(
684
- // Don't more-than-half the size of the selection
685
- Math.max(0.5, scaledSize.x / region.size.x),
686
- Math.max(0.5, scaledSize.y / region.size.y)
687
- ),
688
- region.topLeft
689
- ).rightMul(Mat33.zRotation(
690
- rotationSteps * rotateStepSize, region.center
691
- )).rightMul(Mat33.translation(
692
- Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize)
693
- ));
694
- this.selectionBox.transformPreview(transform);
695
- }
696
-
697
- if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
698
- this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
699
- this.clearSelection();
700
- handled = true;
701
- }
702
-
703
- return handled;
704
- }
705
-
706
- public onKeyUp(evt: KeyUpEvent) {
707
- if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
708
- this.selectionBox.finalizeTransform();
709
- return true;
710
- }
711
- return false;
712
- }
713
-
714
- public onCopy(event: CopyEvent): boolean {
715
- if (!this.selectionBox) {
716
- return false;
717
- }
718
-
719
- const selectedElems = this.selectionBox.getSelectedObjects();
720
- const bbox = this.selectionBox.region;
721
- if (selectedElems.length === 0) {
722
- return false;
723
- }
724
-
725
- const exportViewport = new Viewport(this.editor.notifier);
726
- exportViewport.updateScreenSize(Vec2.of(bbox.w, bbox.h));
727
- exportViewport.resetTransform(Mat33.translation(bbox.topLeft));
728
-
729
- const svgNameSpace = 'http://www.w3.org/2000/svg';
730
- const exportElem = document.createElementNS(svgNameSpace, 'svg');
731
-
732
- const sanitize = true;
733
- const renderer = new SVGRenderer(exportElem, exportViewport, sanitize);
734
-
735
- for (const elem of selectedElems) {
736
- elem.render(renderer);
737
- }
738
-
739
- event.setData('image/svg+xml', exportElem.outerHTML);
740
- return true;
741
- }
742
-
743
- public setEnabled(enabled: boolean) {
744
- super.setEnabled(enabled);
745
-
746
- // Clear the selection
747
- this.handleOverlay.replaceChildren();
748
- this.selectionBox = null;
749
-
750
- this.handleOverlay.style.display = enabled ? 'block' : 'none';
751
-
752
- if (enabled) {
753
- this.handleOverlay.tabIndex = 0;
754
- this.handleOverlay.setAttribute('aria-label', this.editor.localization.selectionToolKeyboardShortcuts);
755
- } else {
756
- this.handleOverlay.tabIndex = -1;
757
- }
758
- }
759
-
760
- // Get the object responsible for displaying this' selection.
761
- public getSelection(): Selection|null {
762
- return this.selectionBox;
763
- }
764
-
765
- public setSelection(objects: AbstractComponent[]) {
766
- let bbox: Rect2|null = null;
767
- for (const object of objects) {
768
- if (bbox) {
769
- bbox = bbox.union(object.getBBox());
770
- } else {
771
- bbox = object.getBBox();
772
- }
773
- }
774
-
775
- if (!bbox) {
776
- return;
777
- }
778
-
779
- this.clearSelection();
780
- if (!this.selectionBox) {
781
- this.makeSelectionBox(bbox.topLeft);
782
- }
783
-
784
- this.selectionBox!.setSelectedObjects(objects, bbox);
785
- }
786
-
787
- public clearSelection() {
788
- this.handleOverlay.replaceChildren();
789
- this.prevSelectionBox = this.selectionBox;
790
- this.selectionBox = null;
791
-
792
- this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
793
- kind: EditorEventType.ToolUpdated,
794
- tool: this,
795
- });
796
- }
797
- }