js-draw 0.3.2 → 0.4.0

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 (73) hide show
  1. package/CHANGELOG.md +9 -1
  2. package/README.md +1 -3
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +11 -0
  5. package/dist/src/Editor.js +104 -76
  6. package/dist/src/Pointer.d.ts +1 -1
  7. package/dist/src/Pointer.js +8 -3
  8. package/dist/src/Viewport.d.ts +1 -0
  9. package/dist/src/Viewport.js +14 -1
  10. package/dist/src/components/ImageComponent.d.ts +2 -2
  11. package/dist/src/language/assertions.d.ts +1 -0
  12. package/dist/src/language/assertions.js +5 -0
  13. package/dist/src/math/Mat33.d.ts +38 -2
  14. package/dist/src/math/Mat33.js +30 -1
  15. package/dist/src/math/Path.d.ts +1 -1
  16. package/dist/src/math/Path.js +10 -8
  17. package/dist/src/math/Vec3.d.ts +11 -1
  18. package/dist/src/math/Vec3.js +15 -0
  19. package/dist/src/math/rounding.d.ts +1 -0
  20. package/dist/src/math/rounding.js +13 -6
  21. package/dist/src/rendering/renderers/AbstractRenderer.js +2 -1
  22. package/dist/src/toolbar/HTMLToolbar.js +5 -4
  23. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
  24. package/dist/src/tools/PasteHandler.js +3 -1
  25. package/dist/src/tools/Pen.js +1 -1
  26. package/dist/src/tools/SelectionTool/Selection.d.ts +54 -0
  27. package/dist/src/tools/SelectionTool/Selection.js +337 -0
  28. package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +35 -0
  29. package/dist/src/tools/SelectionTool/SelectionHandle.js +75 -0
  30. package/dist/src/tools/SelectionTool/SelectionTool.d.ts +31 -0
  31. package/dist/src/tools/SelectionTool/SelectionTool.js +276 -0
  32. package/dist/src/tools/SelectionTool/TransformMode.d.ts +34 -0
  33. package/dist/src/tools/SelectionTool/TransformMode.js +98 -0
  34. package/dist/src/tools/SelectionTool/types.d.ts +9 -0
  35. package/dist/src/tools/SelectionTool/types.js +11 -0
  36. package/dist/src/tools/ToolController.js +1 -1
  37. package/dist/src/tools/lib.d.ts +1 -1
  38. package/dist/src/tools/lib.js +1 -1
  39. package/dist/src/types.d.ts +1 -1
  40. package/package.json +1 -1
  41. package/src/Editor.css +1 -0
  42. package/src/Editor.ts +145 -108
  43. package/src/Pointer.ts +8 -3
  44. package/src/Viewport.ts +17 -2
  45. package/src/components/AbstractComponent.ts +2 -6
  46. package/src/components/ImageComponent.ts +2 -6
  47. package/src/components/Text.ts +2 -6
  48. package/src/language/assertions.ts +6 -0
  49. package/src/math/Mat33.test.ts +14 -0
  50. package/src/math/Mat33.ts +43 -2
  51. package/src/math/Path.toString.test.ts +12 -1
  52. package/src/math/Path.ts +11 -9
  53. package/src/math/Vec3.ts +22 -1
  54. package/src/math/rounding.test.ts +30 -5
  55. package/src/math/rounding.ts +16 -7
  56. package/src/rendering/renderers/AbstractRenderer.ts +3 -2
  57. package/src/toolbar/HTMLToolbar.ts +5 -4
  58. package/src/toolbar/widgets/SelectionToolWidget.ts +1 -1
  59. package/src/tools/PasteHandler.ts +4 -1
  60. package/src/tools/Pen.ts +1 -1
  61. package/src/tools/SelectionTool/Selection.ts +455 -0
  62. package/src/tools/SelectionTool/SelectionHandle.ts +99 -0
  63. package/src/tools/SelectionTool/SelectionTool.css +22 -0
  64. package/src/tools/{SelectionTool.test.ts → SelectionTool/SelectionTool.test.ts} +21 -21
  65. package/src/tools/SelectionTool/SelectionTool.ts +335 -0
  66. package/src/tools/SelectionTool/TransformMode.ts +114 -0
  67. package/src/tools/SelectionTool/types.ts +11 -0
  68. package/src/tools/ToolController.ts +1 -1
  69. package/src/tools/lib.ts +1 -1
  70. package/src/types.ts +1 -1
  71. package/dist/src/tools/SelectionTool.d.ts +0 -65
  72. package/dist/src/tools/SelectionTool.js +0 -647
  73. package/src/tools/SelectionTool.ts +0 -797
@@ -0,0 +1,99 @@
1
+ import { assertUnreachable } from '../../language/assertions';
2
+ import { Point2, Vec2 } from '../../math/Vec2';
3
+ import { cssPrefix } from './SelectionTool';
4
+ import Selection from './Selection';
5
+ import Pointer from '../../Pointer';
6
+
7
+ export enum HandleShape {
8
+ Circle,
9
+ Square,
10
+ }
11
+
12
+ export const handleSize = 30;
13
+
14
+ // `startPoint` is in screen coordinates
15
+ export type DragStartCallback = (startPoint: Point2)=>void;
16
+ export type DragUpdateCallback = (canvasPoint: Point2)=> void;
17
+ export type DragEndCallback = ()=> void;
18
+
19
+ export default class SelectionHandle {
20
+ private element: HTMLElement;
21
+
22
+ // Bounding box in screen coordinates.
23
+
24
+ public constructor(
25
+ readonly shape: HandleShape,
26
+ private readonly parentSide: Vec2,
27
+ private readonly parent: Selection,
28
+
29
+ private readonly onDragStart: DragStartCallback,
30
+ private readonly onDragUpdate: DragUpdateCallback,
31
+ private readonly onDragEnd: DragEndCallback,
32
+ ) {
33
+ this.element = document.createElement('div');
34
+ this.element.classList.add(`${cssPrefix}handle`);
35
+
36
+ switch (shape) {
37
+ case HandleShape.Circle:
38
+ this.element.classList.add(`${cssPrefix}circle`);
39
+ break;
40
+ case HandleShape.Square:
41
+ this.element.classList.add(`${cssPrefix}square`);
42
+ break;
43
+ default:
44
+ assertUnreachable(shape);
45
+ }
46
+
47
+ this.updatePosition();
48
+ }
49
+
50
+ /**
51
+ * Adds this to `container`, where `conatiner` should be the background/selection
52
+ * element visible on the screen.
53
+ */
54
+ public addTo(container: HTMLElement) {
55
+ container.appendChild(this.element);
56
+ }
57
+
58
+ public updatePosition() {
59
+ const parentRect = this.parent.screenRegion;
60
+ const size = Vec2.of(handleSize, handleSize);
61
+ const topLeft = parentRect.size.scale(this.parentSide)
62
+ // Center
63
+ .minus(size.times(1/2));
64
+
65
+ // Position within the selection box.
66
+ this.element.style.marginLeft = `${topLeft.x}px`;
67
+ this.element.style.marginTop = `${topLeft.y}px`;
68
+ this.element.style.width = `${size.x}px`;
69
+ this.element.style.height = `${size.y}px`;
70
+ }
71
+
72
+ /**
73
+ * @returns `true` if the given `EventTarget` matches this.
74
+ */
75
+ public isTarget(target: EventTarget): boolean {
76
+ return target === this.element;
77
+ }
78
+
79
+ private dragLastPos: Vec2|null = null;
80
+ public handleDragStart(pointer: Pointer) {
81
+ this.onDragStart(pointer.canvasPos);
82
+ this.dragLastPos = pointer.canvasPos;
83
+ }
84
+
85
+ public handleDragUpdate(pointer: Pointer) {
86
+ if (!this.dragLastPos) {
87
+ return;
88
+ }
89
+
90
+ this.onDragUpdate(pointer.canvasPos);
91
+ }
92
+
93
+ public handleDragEnd() {
94
+ if (!this.dragLastPos) {
95
+ return;
96
+ }
97
+ this.onDragEnd();
98
+ }
99
+ }
@@ -0,0 +1,22 @@
1
+
2
+ .selection-tool-selection-background {
3
+ background-color: var(--secondary-background-color);
4
+ opacity: 0.8;
5
+ overflow: visible;
6
+ }
7
+
8
+ .selection-tool-handle {
9
+ border: 1px solid var(--primary-foreground-color);
10
+ background: var(--primary-background-color);
11
+ position: absolute;
12
+ cursor: grab;
13
+ }
14
+
15
+ .selection-tool-handle.selection-tool-circle {
16
+ border-radius: 100%;
17
+ }
18
+
19
+ .overlay.handleOverlay {
20
+ height: 0;
21
+ overflow: visible;
22
+ }
@@ -1,21 +1,21 @@
1
- import Color4 from '../Color4';
2
- import Stroke from '../components/Stroke';
3
- import Editor from '../Editor';
4
- import EditorImage from '../EditorImage';
5
- import Path from '../math/Path';
6
- import { Vec2 } from '../math/Vec2';
7
- import { InputEvtType } from '../types';
1
+ import Color4 from '../../Color4';
2
+ import Stroke from '../../components/Stroke';
3
+ import Editor from '../../Editor';
4
+ import EditorImage from '../../EditorImage';
5
+ import Path from '../../math/Path';
6
+ import { Vec2 } from '../../math/Vec2';
7
+ import { InputEvtType } from '../../types';
8
8
  import SelectionTool from './SelectionTool';
9
- import createEditor from '../testing/createEditor';
9
+ import createEditor from '../../testing/createEditor';
10
10
 
11
11
  const getSelectionTool = (editor: Editor): SelectionTool => {
12
12
  return editor.toolController.getMatchingTools(SelectionTool)[0];
13
13
  };
14
14
 
15
- const createSquareStroke = () => {
15
+ const createSquareStroke = (size: number = 1) => {
16
16
  const testStroke = new Stroke([
17
- // A filled unit square
18
- Path.fromString('M0,0 L1,0 L1,1 L0,1 Z').toRenderable({ fill: Color4.blue }),
17
+ // A filled square
18
+ Path.fromString(`M0,0 L${size},0 L${size},${size} L0,${size} Z`).toRenderable({ fill: Color4.blue }),
19
19
  ]);
20
20
  const addTestStrokeCommand = EditorImage.addElement(testStroke);
21
21
 
@@ -46,8 +46,8 @@ describe('SelectionTool', () => {
46
46
  });
47
47
  });
48
48
 
49
- it('dragging the selected region should move selected items', () => {
50
- const { testStroke, addTestStrokeCommand } = createSquareStroke();
49
+ it('sending keyboard events to the selected region should move selected items', () => {
50
+ const { testStroke, addTestStrokeCommand } = createSquareStroke(50);
51
51
  const editor = createEditor();
52
52
  editor.dispatch(addTestStrokeCommand);
53
53
 
@@ -62,13 +62,12 @@ describe('SelectionTool', () => {
62
62
  expect(selection).not.toBeNull();
63
63
 
64
64
  // Drag the object
65
- selection!.handleBackgroundDrag(Vec2.of(5, 5));
66
- selection!.finalizeTransform();
65
+ // (d => move right (d is from WASD controls.))
66
+ editor.sendKeyboardEvent(InputEvtType.KeyPressEvent, 'd');
67
+ editor.sendKeyboardEvent(InputEvtType.KeyPressEvent, 'd');
68
+ editor.sendKeyboardEvent(InputEvtType.KeyUpEvent, 'd');
67
69
 
68
- expect(testStroke.getBBox().topLeft).toMatchObject({
69
- x: 5,
70
- y: 5,
71
- });
70
+ expect(testStroke.getBBox().topLeft.x).toBeGreaterThan(5);
72
71
 
73
72
  editor.history.undo();
74
73
 
@@ -79,7 +78,7 @@ describe('SelectionTool', () => {
79
78
  });
80
79
 
81
80
  it('moving the selection with a keyboard should move the view to keep the selection in view', () => {
82
- const { addTestStrokeCommand } = createSquareStroke();
81
+ const { addTestStrokeCommand } = createSquareStroke(100);
83
82
  const editor = createEditor();
84
83
  editor.dispatch(addTestStrokeCommand);
85
84
 
@@ -97,7 +96,8 @@ describe('SelectionTool', () => {
97
96
  throw new Error('Selection should be non-null.');
98
97
  }
99
98
 
100
- selection.handleBackgroundDrag(Vec2.of(0, -1000));
99
+ editor.sendKeyboardEvent(InputEvtType.KeyPressEvent, 'a');
100
+ editor.sendKeyboardEvent(InputEvtType.KeyUpEvent, 'a');
101
101
  expect(editor.viewport.visibleRect.containsPoint(selection.region.center)).toBe(true);
102
102
  });
103
103
  });
@@ -0,0 +1,335 @@
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 AbstractComponent from '../../components/AbstractComponent';
6
+ import Editor from '../../Editor';
7
+ import Mat33 from '../../math/Mat33';
8
+ import Rect2 from '../../math/Rect2';
9
+ import { Point2, Vec2 } from '../../math/Vec2';
10
+ import { CopyEvent, EditorEventType, KeyPressEvent, KeyUpEvent, PointerEvt } from '../../types';
11
+ import Viewport from '../../Viewport';
12
+ import BaseTool from '../BaseTool';
13
+ import SVGRenderer from '../../rendering/renderers/SVGRenderer';
14
+ import Selection from './Selection';
15
+
16
+ export const cssPrefix = 'selection-tool-';
17
+
18
+ // {@inheritDoc SelectionTool!}
19
+ export default class SelectionTool extends BaseTool {
20
+ private handleOverlay: HTMLElement;
21
+ private prevSelectionBox: Selection|null;
22
+ private selectionBox: Selection|null;
23
+ private lastEvtTarget: EventTarget|null = null;
24
+
25
+ public constructor(private editor: Editor, description: string) {
26
+ super(editor.notifier, description);
27
+
28
+ this.handleOverlay = document.createElement('div');
29
+ editor.createHTMLOverlay(this.handleOverlay);
30
+
31
+ this.handleOverlay.style.display = 'none';
32
+ this.handleOverlay.classList.add('handleOverlay');
33
+
34
+ editor.notifier.on(EditorEventType.ViewportChanged, _data => {
35
+ this.selectionBox?.updateUI();
36
+ });
37
+
38
+ this.editor.handleKeyEventsFrom(this.handleOverlay);
39
+ this.editor.handlePointerEventsFrom(this.handleOverlay, (eventName, htmlEvent: PointerEvent) => {
40
+ if (eventName === 'pointerdown') {
41
+ this.lastEvtTarget = htmlEvent.target;
42
+ }
43
+ return true;
44
+ });
45
+ }
46
+
47
+ private makeSelectionBox(selectionStartPos: Point2) {
48
+ this.prevSelectionBox = this.selectionBox;
49
+ this.selectionBox = new Selection(
50
+ selectionStartPos, this.editor
51
+ );
52
+ // Remove any previous selection rects
53
+ this.handleOverlay.replaceChildren();
54
+ this.selectionBox.addTo(this.handleOverlay);
55
+ }
56
+
57
+ private selectionBoxHandlingEvt: boolean = false;
58
+ public onPointerDown(event: PointerEvt): boolean {
59
+ if (event.allPointers.length === 1 && event.current.isPrimary) {
60
+ if (this.lastEvtTarget && this.selectionBox?.onDragStart(event.current, this.lastEvtTarget)) {
61
+ this.selectionBoxHandlingEvt = true;
62
+ } else {
63
+ this.makeSelectionBox(event.current.canvasPos);
64
+ }
65
+
66
+ return true;
67
+ }
68
+ return false;
69
+ }
70
+
71
+ public onPointerMove(event: PointerEvt): void {
72
+ if (!this.selectionBox) return;
73
+
74
+ if (this.selectionBoxHandlingEvt) {
75
+ this.selectionBox.onDragUpdate(event.current);
76
+ } else {
77
+ this.selectionBox!.setToPoint(event.current.canvasPos);
78
+ }
79
+ }
80
+
81
+ private onSelectionUpdated() {
82
+ // Note that the selection has changed
83
+ this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
84
+ kind: EditorEventType.ToolUpdated,
85
+ tool: this,
86
+ });
87
+
88
+ const selectedItemCount = this.selectionBox?.getSelectedItemCount() ?? 0;
89
+ if (selectedItemCount > 0) {
90
+ this.editor.announceForAccessibility(
91
+ this.editor.localization.selectedElements(selectedItemCount)
92
+ );
93
+ this.zoomToSelection();
94
+ } else if (this.selectionBox) {
95
+ this.selectionBox.cancelSelection();
96
+ this.prevSelectionBox = this.selectionBox;
97
+ this.selectionBox = null;
98
+ }
99
+ }
100
+
101
+ private onGestureEnd() {
102
+ this.lastEvtTarget = null;
103
+
104
+ if (!this.selectionBox) return;
105
+
106
+ if (!this.selectionBoxHandlingEvt) {
107
+ // Expand/shrink the selection rectangle, if applicable
108
+ this.selectionBox.resolveToObjects();
109
+ this.onSelectionUpdated();
110
+ } else {
111
+ this.selectionBox.onDragEnd();
112
+ }
113
+
114
+
115
+ this.selectionBoxHandlingEvt = false;
116
+ }
117
+
118
+ private zoomToSelection() {
119
+ if (this.selectionBox) {
120
+ const selectionRect = this.selectionBox.region;
121
+ this.editor.dispatchNoAnnounce(this.editor.viewport.zoomTo(selectionRect, false), false);
122
+ }
123
+ }
124
+
125
+ public onPointerUp(event: PointerEvt): void {
126
+ if (!this.selectionBox) return;
127
+
128
+ this.selectionBox.setToPoint(event.current.canvasPos);
129
+ this.onGestureEnd();
130
+ }
131
+
132
+ public onGestureCancel(): void {
133
+ if (this.selectionBoxHandlingEvt) {
134
+ this.selectionBox?.onDragCancel();
135
+ } else {
136
+ // Revert to the previous selection, if any.
137
+ this.selectionBox?.cancelSelection();
138
+ this.selectionBox = this.prevSelectionBox;
139
+ this.selectionBox?.addTo(this.handleOverlay);
140
+ }
141
+ }
142
+
143
+ private static handleableKeys = [
144
+ 'a', 'h', 'ArrowLeft',
145
+ 'd', 'l', 'ArrowRight',
146
+ 'q', 'k', 'ArrowUp',
147
+ 'e', 'j', 'ArrowDown',
148
+ 'r', 'R',
149
+ 'i', 'I', 'o', 'O',
150
+ ];
151
+ public onKeyPress(event: KeyPressEvent): boolean {
152
+ let rotationSteps = 0;
153
+ let xTranslateSteps = 0;
154
+ let yTranslateSteps = 0;
155
+ let xScaleSteps = 0;
156
+ let yScaleSteps = 0;
157
+
158
+ switch (event.key) {
159
+ case 'a':
160
+ case 'h':
161
+ case 'ArrowLeft':
162
+ xTranslateSteps -= 1;
163
+ break;
164
+ case 'd':
165
+ case 'l':
166
+ case 'ArrowRight':
167
+ xTranslateSteps += 1;
168
+ break;
169
+ case 'q':
170
+ case 'k':
171
+ case 'ArrowUp':
172
+ yTranslateSteps -= 1;
173
+ break;
174
+ case 'e':
175
+ case 'j':
176
+ case 'ArrowDown':
177
+ yTranslateSteps += 1;
178
+ break;
179
+ case 'r':
180
+ rotationSteps += 1;
181
+ break;
182
+ case 'R':
183
+ rotationSteps -= 1;
184
+ break;
185
+ case 'i':
186
+ xScaleSteps -= 1;
187
+ break;
188
+ case 'I':
189
+ xScaleSteps += 1;
190
+ break;
191
+ case 'o':
192
+ yScaleSteps -= 1;
193
+ break;
194
+ case 'O':
195
+ yScaleSteps += 1;
196
+ break;
197
+ }
198
+
199
+ let handled = xTranslateSteps !== 0
200
+ || yTranslateSteps !== 0
201
+ || rotationSteps !== 0
202
+ || xScaleSteps !== 0
203
+ || yScaleSteps !== 0;
204
+
205
+ if (!this.selectionBox) {
206
+ handled = false;
207
+ } else if (handled) {
208
+ const translateStepSize = 10 * this.editor.viewport.getSizeOfPixelOnCanvas();
209
+ const rotateStepSize = Math.PI / 8;
210
+ const scaleStepSize = 5 / 4;
211
+
212
+ const region = this.selectionBox.region;
213
+ const scaleFactor = Vec2.of(scaleStepSize ** xScaleSteps, scaleStepSize ** yScaleSteps);
214
+
215
+ const rotationMat = Mat33.zRotation(
216
+ rotationSteps * rotateStepSize
217
+ );
218
+ const roundedRotationMatrix = rotationMat.mapEntries(component => Viewport.roundScaleRatio(component));
219
+ const regionCenter = this.editor.viewport.roundPoint(region.center);
220
+
221
+ const transform = Mat33.scaling2D(
222
+ scaleFactor,
223
+ this.editor.viewport.roundPoint(region.topLeft)
224
+ ).rightMul(
225
+ Mat33.translation(regionCenter).rightMul(
226
+ roundedRotationMatrix
227
+ ).rightMul(
228
+ Mat33.translation(regionCenter.times(-1))
229
+ )
230
+ ).rightMul(Mat33.translation(
231
+ this.editor.viewport.roundPoint(Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize))
232
+ ));
233
+ const oldTransform = this.selectionBox.getTransform();
234
+ this.selectionBox.setTransform(oldTransform.rightMul(transform));
235
+ }
236
+
237
+ if (this.selectionBox && !handled && (event.key === 'Delete' || event.key === 'Backspace')) {
238
+ this.editor.dispatch(this.selectionBox.deleteSelectedObjects());
239
+ this.clearSelection();
240
+ handled = true;
241
+ }
242
+
243
+ return handled;
244
+ }
245
+
246
+ public onKeyUp(evt: KeyUpEvent) {
247
+ if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
248
+ this.selectionBox.finalizeTransform();
249
+ return true;
250
+ }
251
+ return false;
252
+ }
253
+
254
+ public onCopy(event: CopyEvent): boolean {
255
+ if (!this.selectionBox) {
256
+ return false;
257
+ }
258
+
259
+ const selectedElems = this.selectionBox.getSelectedObjects();
260
+ const bbox = this.selectionBox.region;
261
+ if (selectedElems.length === 0) {
262
+ return false;
263
+ }
264
+
265
+ const exportViewport = new Viewport(this.editor.notifier);
266
+ exportViewport.updateScreenSize(Vec2.of(bbox.w, bbox.h));
267
+ exportViewport.resetTransform(Mat33.translation(bbox.topLeft));
268
+
269
+ const svgNameSpace = 'http://www.w3.org/2000/svg';
270
+ const exportElem = document.createElementNS(svgNameSpace, 'svg');
271
+
272
+ const sanitize = true;
273
+ const renderer = new SVGRenderer(exportElem, exportViewport, sanitize);
274
+
275
+ for (const elem of selectedElems) {
276
+ elem.render(renderer);
277
+ }
278
+
279
+ event.setData('image/svg+xml', exportElem.outerHTML);
280
+ return true;
281
+ }
282
+
283
+ public setEnabled(enabled: boolean) {
284
+ super.setEnabled(enabled);
285
+
286
+ // Clear the selection
287
+ this.handleOverlay.replaceChildren();
288
+ this.selectionBox = null;
289
+
290
+ this.handleOverlay.style.display = enabled ? 'block' : 'none';
291
+
292
+ if (enabled) {
293
+ this.handleOverlay.tabIndex = 0;
294
+ this.handleOverlay.setAttribute('aria-label', this.editor.localization.selectionToolKeyboardShortcuts);
295
+ } else {
296
+ this.handleOverlay.tabIndex = -1;
297
+ }
298
+ }
299
+
300
+ // Get the object responsible for displaying this' selection.
301
+ public getSelection(): Selection|null {
302
+ return this.selectionBox;
303
+ }
304
+
305
+ public setSelection(objects: AbstractComponent[]) {
306
+ let bbox: Rect2|null = null;
307
+ for (const object of objects) {
308
+ if (bbox) {
309
+ bbox = bbox.union(object.getBBox());
310
+ } else {
311
+ bbox = object.getBBox();
312
+ }
313
+ }
314
+
315
+ if (!bbox) {
316
+ return;
317
+ }
318
+
319
+ this.clearSelection();
320
+ if (!this.selectionBox) {
321
+ this.makeSelectionBox(bbox.topLeft);
322
+ }
323
+
324
+ this.selectionBox!.setSelectedObjects(objects, bbox);
325
+ this.onSelectionUpdated();
326
+ }
327
+
328
+ public clearSelection() {
329
+ this.handleOverlay.replaceChildren();
330
+ this.prevSelectionBox = this.selectionBox;
331
+ this.selectionBox = null;
332
+
333
+ this.onSelectionUpdated();
334
+ }
335
+ }
@@ -0,0 +1,114 @@
1
+ import Editor from '../../Editor';
2
+ import Mat33 from '../../math/Mat33';
3
+ import { Point2, Vec2 } from '../../math/Vec2';
4
+ import Vec3 from '../../math/Vec3';
5
+ import Viewport from '../../Viewport';
6
+ import Selection from './Selection';
7
+ import { ResizeMode } from './types';
8
+
9
+ export class DragTransformer {
10
+ private dragStartPoint: Point2;
11
+ public constructor(private readonly editor: Editor, private selection: Selection) { }
12
+
13
+ public onDragStart(startPoint: Vec3) {
14
+ this.selection.setTransform(Mat33.identity);
15
+ this.dragStartPoint = startPoint;
16
+ }
17
+ public onDragUpdate(canvasPos: Vec3) {
18
+ const delta = this.editor.viewport.roundPoint(canvasPos.minus(this.dragStartPoint));
19
+ this.selection.setTransform(Mat33.translation(
20
+ delta
21
+ ));
22
+ }
23
+ public onDragEnd() {
24
+ this.selection.finalizeTransform();
25
+ }
26
+ }
27
+
28
+ export class ResizeTransformer {
29
+ private mode: ResizeMode = ResizeMode.Both;
30
+ private dragStartPoint: Point2;
31
+ public constructor(private readonly editor: Editor, private selection: Selection) { }
32
+
33
+ public onDragStart(startPoint: Vec3, mode: ResizeMode) {
34
+ this.selection.setTransform(Mat33.identity);
35
+ this.mode = mode;
36
+ this.dragStartPoint = startPoint;
37
+ }
38
+ public onDragUpdate(canvasPos: Vec3) {
39
+ const canvasDelta = canvasPos.minus(this.dragStartPoint);
40
+
41
+ const origWidth = this.selection.preTransformRegion.width;
42
+ const origHeight = this.selection.preTransformRegion.height;
43
+
44
+ let scale = Vec2.of(1, 1);
45
+ if (this.mode === ResizeMode.HorizontalOnly) {
46
+ const newWidth = origWidth + canvasDelta.x;
47
+ scale = Vec2.of(newWidth / origWidth, scale.y);
48
+ }
49
+
50
+ if (this.mode === ResizeMode.VerticalOnly) {
51
+ const newHeight = origHeight + canvasDelta.y;
52
+ scale = Vec2.of(scale.x, newHeight / origHeight);
53
+ }
54
+
55
+ if (this.mode === ResizeMode.Both) {
56
+ const delta = Math.abs(canvasDelta.x) > Math.abs(canvasDelta.y) ? canvasDelta.x : canvasDelta.y;
57
+ const newWidth = origWidth + delta;
58
+ scale = Vec2.of(newWidth / origWidth, newWidth / origWidth);
59
+ }
60
+
61
+ // Round: If this isn't done, scaling can create numbers with long decimal representations.
62
+ // long decimal representations => large file sizes.
63
+ scale = scale.map(component => Viewport.roundScaleRatio(component, 2));
64
+
65
+ if (scale.x > 0 && scale.y > 0) {
66
+ const origin = this.editor.viewport.roundPoint(this.selection.preTransformRegion.topLeft);
67
+ this.selection.setTransform(Mat33.scaling2D(scale, origin));
68
+ }
69
+ }
70
+ public onDragEnd() {
71
+ this.selection.finalizeTransform();
72
+ }
73
+ }
74
+
75
+ export class RotateTransformer {
76
+ private startAngle: number = 0;
77
+ public constructor(private readonly editor: Editor, private selection: Selection) { }
78
+
79
+ private getAngle(canvasPoint: Point2) {
80
+ const selectionCenter = this.selection.preTransformRegion.center;
81
+ const offset = canvasPoint.minus(selectionCenter);
82
+ return offset.angle();
83
+ }
84
+
85
+ private roundAngle(angle: number) {
86
+ // Round angles to the nearest 16th of a turn
87
+ const roundingFactor = 16 / 2 / Math.PI;
88
+ return Math.round(angle * roundingFactor) / roundingFactor;
89
+ }
90
+
91
+ public onDragStart(startPoint: Vec3) {
92
+ this.selection.setTransform(Mat33.identity);
93
+ this.startAngle = this.getAngle(startPoint);
94
+ }
95
+
96
+ public onDragUpdate(canvasPos: Vec3) {
97
+ const targetRotation = this.roundAngle(this.getAngle(canvasPos) - this.startAngle);
98
+
99
+ // Transform in canvas space
100
+ const canvasSelCenter = this.editor.viewport.roundPoint(this.selection.preTransformRegion.center);
101
+ const unrounded = Mat33.zRotation(targetRotation);
102
+ const roundedRotationTransform = unrounded.mapEntries(entry => Viewport.roundScaleRatio(entry));
103
+
104
+ const fullRoundedTransform = Mat33
105
+ .translation(canvasSelCenter)
106
+ .rightMul(roundedRotationTransform)
107
+ .rightMul(Mat33.translation(canvasSelCenter.times(-1)));
108
+
109
+ this.selection.setTransform(fullRoundedTransform);
110
+ }
111
+ public onDragEnd() {
112
+ this.selection.finalizeTransform();
113
+ }
114
+ }
@@ -0,0 +1,11 @@
1
+
2
+ export enum ResizeMode {
3
+ Both,
4
+ HorizontalOnly,
5
+ VerticalOnly,
6
+ }
7
+
8
+ export enum TransformMode {
9
+ Snap,
10
+ NoSnap,
11
+ }
@@ -5,7 +5,7 @@ import PanZoom, { PanZoomMode } from './PanZoom';
5
5
  import Pen from './Pen';
6
6
  import ToolEnabledGroup from './ToolEnabledGroup';
7
7
  import Eraser from './Eraser';
8
- import SelectionTool from './SelectionTool';
8
+ import SelectionTool from './SelectionTool/SelectionTool';
9
9
  import Color4 from '../Color4';
10
10
  import { ToolLocalization } from './localization';
11
11
  import UndoRedoShortcut from './UndoRedoShortcut';
package/src/tools/lib.ts CHANGED
@@ -13,7 +13,7 @@ export { default as PanZoomTool, PanZoomMode } from './PanZoom';
13
13
 
14
14
  export { default as PenTool, PenStyle } from './Pen';
15
15
  export { default as TextTool } from './TextTool';
16
- export { default as SelectionTool } from './SelectionTool';
16
+ export { default as SelectionTool } from './SelectionTool/SelectionTool';
17
17
  export { default as EraserTool } from './Eraser';
18
18
  export { default as PasteHandler } from './PasteHandler';
19
19
 
package/src/types.ts CHANGED
@@ -10,7 +10,7 @@ import Rect2 from './math/Rect2';
10
10
  import Pointer from './Pointer';
11
11
  import Color4 from './Color4';
12
12
  import Command from './commands/Command';
13
- import { BaseWidget } from './lib';
13
+ import BaseWidget from './toolbar/widgets/BaseWidget';
14
14
 
15
15
 
16
16
  export interface PointerEvtListener {