js-draw 0.3.1 → 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 (132) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.md +4 -1
  2. package/CHANGELOG.md +16 -0
  3. package/README.md +1 -3
  4. package/dist/bundle.js +1 -1
  5. package/dist/src/Editor.d.ts +15 -1
  6. package/dist/src/Editor.js +221 -78
  7. package/dist/src/EditorImage.js +4 -1
  8. package/dist/src/Pointer.d.ts +1 -1
  9. package/dist/src/Pointer.js +8 -3
  10. package/dist/src/SVGLoader.d.ts +4 -1
  11. package/dist/src/SVGLoader.js +78 -33
  12. package/dist/src/UndoRedoHistory.d.ts +1 -0
  13. package/dist/src/UndoRedoHistory.js +6 -0
  14. package/dist/src/Viewport.d.ts +2 -0
  15. package/dist/src/Viewport.js +26 -5
  16. package/dist/src/commands/lib.d.ts +2 -1
  17. package/dist/src/commands/lib.js +2 -1
  18. package/dist/src/commands/localization.d.ts +1 -0
  19. package/dist/src/commands/localization.js +1 -0
  20. package/dist/src/commands/uniteCommands.d.ts +4 -0
  21. package/dist/src/commands/uniteCommands.js +105 -0
  22. package/dist/src/components/AbstractComponent.d.ts +2 -0
  23. package/dist/src/components/AbstractComponent.js +41 -5
  24. package/dist/src/components/ImageComponent.d.ts +27 -0
  25. package/dist/src/components/ImageComponent.js +129 -0
  26. package/dist/src/components/builders/FreehandLineBuilder.js +2 -2
  27. package/dist/src/components/lib.d.ts +4 -2
  28. package/dist/src/components/lib.js +4 -2
  29. package/dist/src/components/localization.d.ts +2 -0
  30. package/dist/src/components/localization.js +2 -0
  31. package/dist/src/language/assertions.d.ts +1 -0
  32. package/dist/src/language/assertions.js +5 -0
  33. package/dist/src/math/LineSegment2.d.ts +2 -0
  34. package/dist/src/math/LineSegment2.js +3 -0
  35. package/dist/src/math/Mat33.d.ts +38 -2
  36. package/dist/src/math/Mat33.js +30 -1
  37. package/dist/src/math/Path.d.ts +1 -1
  38. package/dist/src/math/Path.js +10 -8
  39. package/dist/src/math/Vec3.d.ts +11 -1
  40. package/dist/src/math/Vec3.js +15 -0
  41. package/dist/src/math/rounding.d.ts +1 -0
  42. package/dist/src/math/rounding.js +13 -6
  43. package/dist/src/rendering/localization.d.ts +3 -0
  44. package/dist/src/rendering/localization.js +3 -0
  45. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -0
  46. package/dist/src/rendering/renderers/AbstractRenderer.js +2 -1
  47. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  48. package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
  49. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
  50. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  51. package/dist/src/rendering/renderers/SVGRenderer.d.ts +5 -2
  52. package/dist/src/rendering/renderers/SVGRenderer.js +45 -20
  53. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
  54. package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
  55. package/dist/src/toolbar/HTMLToolbar.js +5 -4
  56. package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
  57. package/dist/src/tools/BaseTool.d.ts +3 -1
  58. package/dist/src/tools/BaseTool.js +6 -0
  59. package/dist/src/tools/PasteHandler.d.ts +16 -0
  60. package/dist/src/tools/PasteHandler.js +144 -0
  61. package/dist/src/tools/Pen.js +1 -1
  62. package/dist/src/tools/SelectionTool/Selection.d.ts +54 -0
  63. package/dist/src/tools/SelectionTool/Selection.js +337 -0
  64. package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +35 -0
  65. package/dist/src/tools/SelectionTool/SelectionHandle.js +75 -0
  66. package/dist/src/tools/SelectionTool/SelectionTool.d.ts +31 -0
  67. package/dist/src/tools/SelectionTool/SelectionTool.js +276 -0
  68. package/dist/src/tools/SelectionTool/TransformMode.d.ts +34 -0
  69. package/dist/src/tools/SelectionTool/TransformMode.js +98 -0
  70. package/dist/src/tools/SelectionTool/types.d.ts +9 -0
  71. package/dist/src/tools/SelectionTool/types.js +11 -0
  72. package/dist/src/tools/ToolController.js +37 -28
  73. package/dist/src/tools/lib.d.ts +2 -1
  74. package/dist/src/tools/lib.js +2 -1
  75. package/dist/src/tools/localization.d.ts +3 -0
  76. package/dist/src/tools/localization.js +3 -0
  77. package/dist/src/types.d.ts +14 -3
  78. package/dist/src/types.js +2 -0
  79. package/package.json +1 -1
  80. package/src/Editor.css +1 -0
  81. package/src/Editor.ts +275 -109
  82. package/src/EditorImage.ts +7 -1
  83. package/src/Pointer.ts +8 -3
  84. package/src/SVGLoader.ts +90 -36
  85. package/src/UndoRedoHistory.test.ts +33 -0
  86. package/src/UndoRedoHistory.ts +8 -0
  87. package/src/Viewport.ts +30 -6
  88. package/src/commands/lib.ts +2 -0
  89. package/src/commands/localization.ts +2 -0
  90. package/src/commands/uniteCommands.test.ts +23 -0
  91. package/src/commands/uniteCommands.ts +121 -0
  92. package/src/components/AbstractComponent.ts +53 -11
  93. package/src/components/ImageComponent.ts +149 -0
  94. package/src/components/Text.ts +2 -6
  95. package/src/components/builders/FreehandLineBuilder.ts +2 -2
  96. package/src/components/lib.ts +7 -2
  97. package/src/components/localization.ts +4 -0
  98. package/src/language/assertions.ts +6 -0
  99. package/src/math/LineSegment2.test.ts +9 -0
  100. package/src/math/LineSegment2.ts +5 -0
  101. package/src/math/Mat33.test.ts +14 -0
  102. package/src/math/Mat33.ts +43 -2
  103. package/src/math/Path.toString.test.ts +12 -1
  104. package/src/math/Path.ts +11 -9
  105. package/src/math/Vec3.ts +22 -1
  106. package/src/math/rounding.test.ts +30 -5
  107. package/src/math/rounding.ts +16 -7
  108. package/src/rendering/localization.ts +6 -0
  109. package/src/rendering/renderers/AbstractRenderer.ts +19 -2
  110. package/src/rendering/renderers/CanvasRenderer.ts +10 -1
  111. package/src/rendering/renderers/DummyRenderer.ts +6 -1
  112. package/src/rendering/renderers/SVGRenderer.ts +50 -21
  113. package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
  114. package/src/toolbar/HTMLToolbar.ts +5 -4
  115. package/src/toolbar/widgets/SelectionToolWidget.ts +1 -1
  116. package/src/tools/BaseTool.ts +9 -1
  117. package/src/tools/PasteHandler.ts +159 -0
  118. package/src/tools/Pen.ts +1 -1
  119. package/src/tools/SelectionTool/Selection.ts +455 -0
  120. package/src/tools/SelectionTool/SelectionHandle.ts +99 -0
  121. package/src/tools/SelectionTool/SelectionTool.css +22 -0
  122. package/src/tools/{SelectionTool.test.ts → SelectionTool/SelectionTool.test.ts} +21 -21
  123. package/src/tools/SelectionTool/SelectionTool.ts +335 -0
  124. package/src/tools/SelectionTool/TransformMode.ts +114 -0
  125. package/src/tools/SelectionTool/types.ts +11 -0
  126. package/src/tools/ToolController.ts +52 -45
  127. package/src/tools/lib.ts +2 -1
  128. package/src/tools/localization.ts +8 -0
  129. package/src/types.ts +17 -3
  130. package/dist/src/tools/SelectionTool.d.ts +0 -59
  131. package/dist/src/tools/SelectionTool.js +0 -589
  132. package/src/tools/SelectionTool.ts +0 -725
@@ -0,0 +1,455 @@
1
+ /**
2
+ * @internal
3
+ * @packageDocumentation
4
+ */
5
+
6
+ import SerializableCommand from '../../commands/SerializableCommand';
7
+ import Editor from '../../Editor';
8
+ import { Mat33, Rect2 } from '../../math/lib';
9
+ import { Point2, Vec2 } from '../../math/Vec2';
10
+ import Pointer from '../../Pointer';
11
+ import SelectionHandle, { HandleShape, handleSize } from './SelectionHandle';
12
+ import { cssPrefix } from './SelectionTool';
13
+ import AbstractComponent from '../../components/AbstractComponent';
14
+ import { Mat33Array } from '../../math/Mat33';
15
+ import { EditorLocalization } from '../../localization';
16
+ import Viewport from '../../Viewport';
17
+ import Erase from '../../commands/Erase';
18
+ import Duplicate from '../../commands/Duplicate';
19
+ import Command from '../../commands/Command';
20
+ import { DragTransformer, ResizeTransformer, RotateTransformer } from './TransformMode';
21
+ import { ResizeMode } from './types';
22
+
23
+ const updateChunkSize = 100;
24
+
25
+ // @internal
26
+ export default class Selection {
27
+ private handles: SelectionHandle[];
28
+ private originalRegion: Rect2;
29
+
30
+ private transformers;
31
+
32
+ private transform: Mat33 = Mat33.identity;
33
+ private transformCommands: SerializableCommand[] = [];
34
+
35
+ private selectedElems: AbstractComponent[] = [];
36
+
37
+ private container: HTMLElement;
38
+ private backgroundElem: HTMLElement;
39
+
40
+ public constructor(startPoint: Point2, private editor: Editor) {
41
+ this.originalRegion = new Rect2(startPoint.x, startPoint.y, 0, 0);
42
+ this.transformers = {
43
+ drag: new DragTransformer(editor, this),
44
+ resize: new ResizeTransformer(editor, this),
45
+ rotate: new RotateTransformer(editor, this),
46
+ };
47
+
48
+ this.container = document.createElement('div');
49
+ this.backgroundElem = document.createElement('div');
50
+ this.backgroundElem.classList.add(`${cssPrefix}selection-background`);
51
+ this.container.appendChild(this.backgroundElem);
52
+
53
+ const resizeHorizontalHandle = new SelectionHandle(
54
+ HandleShape.Square,
55
+ Vec2.of(1, 0.5),
56
+ this,
57
+ (startPoint) => this.transformers.resize.onDragStart(startPoint, ResizeMode.HorizontalOnly),
58
+ (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint),
59
+ () => this.transformers.resize.onDragEnd(),
60
+ );
61
+
62
+ const resizeVerticalHandle = new SelectionHandle(
63
+ HandleShape.Square,
64
+ Vec2.of(0.5, 1),
65
+ this,
66
+ (startPoint) => this.transformers.resize.onDragStart(startPoint, ResizeMode.VerticalOnly),
67
+ (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint),
68
+ () => this.transformers.resize.onDragEnd(),
69
+ );
70
+
71
+ const resizeBothHandle = new SelectionHandle(
72
+ HandleShape.Square,
73
+ Vec2.of(1, 1),
74
+ this,
75
+ (startPoint) => this.transformers.resize.onDragStart(startPoint, ResizeMode.Both),
76
+ (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint),
77
+ () => this.transformers.resize.onDragEnd(),
78
+ );
79
+
80
+ const rotationHandle = new SelectionHandle(
81
+ HandleShape.Circle,
82
+ Vec2.of(0.5, 0),
83
+ this,
84
+ (startPoint) => this.transformers.rotate.onDragStart(startPoint),
85
+ (currentPoint) => this.transformers.rotate.onDragUpdate(currentPoint),
86
+ () => this.transformers.rotate.onDragEnd(),
87
+ );
88
+
89
+ this.handles = [
90
+ resizeBothHandle,
91
+ resizeHorizontalHandle,
92
+ resizeVerticalHandle,
93
+ rotationHandle,
94
+ ];
95
+
96
+ for (const handle of this.handles) {
97
+ handle.addTo(this.backgroundElem);
98
+ }
99
+ }
100
+
101
+ public getTransform(): Mat33 {
102
+ return this.transform;
103
+ }
104
+
105
+ public get preTransformRegion(): Rect2 {
106
+ return this.originalRegion;
107
+ }
108
+
109
+ public get region(): Rect2 {
110
+ // TODO: This currently assumes that the region rotates about its center.
111
+ // This may not be true.
112
+ const rotationMatrix = Mat33.zRotation(this.regionRotation, this.originalRegion.center);
113
+ const scaleAndTranslateMat = this.transform.rightMul(rotationMatrix.inverse());
114
+ return this.originalRegion.transformedBoundingBox(scaleAndTranslateMat);
115
+ }
116
+
117
+ public get regionRotation(): number {
118
+ return this.transform.transformVec3(Vec2.unitX).angle();
119
+ }
120
+
121
+ public get preTransformedScreenRegion(): Rect2 {
122
+ const toScreen = (vec: Point2) => this.editor.viewport.canvasToScreen(vec);
123
+ return Rect2.fromCorners(
124
+ toScreen(this.preTransformRegion.topLeft),
125
+ toScreen(this.preTransformRegion.bottomRight)
126
+ );
127
+ }
128
+
129
+ public get preTransformedScreenRegionRotation(): number {
130
+ return this.editor.viewport.getRotationAngle();
131
+ }
132
+
133
+ public get screenRegion(): Rect2 {
134
+ const toScreen = this.editor.viewport.canvasToScreenTransform;
135
+ const scaleFactor = this.editor.viewport.getScaleFactor();
136
+
137
+ const screenCenter = toScreen.transformVec2(this.region.center);
138
+
139
+ return new Rect2(
140
+ screenCenter.x, screenCenter.y, scaleFactor * this.region.width, scaleFactor * this.region.height
141
+ ).translatedBy(this.region.size.times(-scaleFactor/2));
142
+ }
143
+
144
+ public get screenRegionRotation(): number {
145
+ return this.regionRotation + this.editor.viewport.getRotationAngle();
146
+ }
147
+
148
+ private computeTransformCommands(): SerializableCommand[] {
149
+ return this.selectedElems.map(elem => {
150
+ return elem.transformBy(this.transform);
151
+ });
152
+ }
153
+
154
+ // Applies, previews, but doesn't finalize the given transformation.
155
+ public setTransform(transform: Mat33, preview: boolean = true) {
156
+ this.transform = transform;
157
+
158
+ if (preview) {
159
+ this.previewTransformCmds();
160
+ this.scrollTo();
161
+ }
162
+ }
163
+
164
+ // Applies the current transformation to the selection
165
+ public finalizeTransform() {
166
+ this.transformCommands.forEach(cmd => {
167
+ cmd.unapply(this.editor);
168
+ });
169
+
170
+ const fullTransform = this.transform;
171
+ const currentTransfmCommands = this.computeTransformCommands();
172
+
173
+ // Reset for the next drag
174
+ this.transformCommands = [];
175
+ this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
176
+ this.transform = Mat33.identity;
177
+
178
+ // Make the commands undo-able
179
+ this.editor.dispatch(new Selection.ApplyTransformationCommand(
180
+ this, currentTransfmCommands, fullTransform
181
+ ));
182
+ }
183
+
184
+ static {
185
+ SerializableCommand.register('selection-tool-transform', (json: any, editor) => {
186
+ // The selection box is lost when serializing/deserializing. No need to store box rotation
187
+ const fullTransform: Mat33 = new Mat33(...(json.transform as Mat33Array));
188
+ const commands = (json.commands as any[]).map(data => SerializableCommand.deserialize(data, editor));
189
+
190
+ return new this.ApplyTransformationCommand(null, commands, fullTransform);
191
+ });
192
+ }
193
+
194
+ private static ApplyTransformationCommand = class extends SerializableCommand {
195
+ public constructor(
196
+ private selection: Selection|null,
197
+ private currentTransfmCommands: SerializableCommand[],
198
+ private fullTransform: Mat33,
199
+ ) {
200
+ super('selection-tool-transform');
201
+ }
202
+
203
+ public async apply(editor: Editor) {
204
+ this.selection?.setTransform(this.fullTransform, false);
205
+ this.selection?.updateUI();
206
+ await editor.asyncApplyCommands(this.currentTransfmCommands, updateChunkSize);
207
+ this.selection?.setTransform(Mat33.identity, false);
208
+ this.selection?.recomputeRegion();
209
+ this.selection?.updateUI();
210
+ }
211
+
212
+ public async unapply(editor: Editor) {
213
+ this.selection?.setTransform(this.fullTransform.inverse(), false);
214
+ this.selection?.updateUI();
215
+
216
+ await editor.asyncUnapplyCommands(this.currentTransfmCommands, updateChunkSize);
217
+ this.selection?.setTransform(Mat33.identity);
218
+ this.selection?.recomputeRegion();
219
+ this.selection?.updateUI();
220
+ }
221
+
222
+ protected serializeToJSON() {
223
+ return {
224
+ commands: this.currentTransfmCommands.map(command => command.serialize()),
225
+ transform: this.fullTransform.toArray(),
226
+ };
227
+ }
228
+
229
+ public description(_editor: Editor, localizationTable: EditorLocalization) {
230
+ return localizationTable.transformedElements(this.currentTransfmCommands.length);
231
+ }
232
+ };
233
+
234
+ // Preview the effects of the current transformation on the selection
235
+ private previewTransformCmds() {
236
+ // Don't render what we're moving if it's likely to be slow.
237
+ if (this.selectedElems.length > updateChunkSize) {
238
+ this.updateUI();
239
+ return;
240
+ }
241
+
242
+ this.transformCommands.forEach(cmd => cmd.unapply(this.editor));
243
+ this.transformCommands = this.computeTransformCommands();
244
+ this.transformCommands.forEach(cmd => cmd.apply(this.editor));
245
+
246
+ this.updateUI();
247
+ }
248
+
249
+ // Find the objects corresponding to this in the document,
250
+ // select them.
251
+ // Returns false iff nothing was selected.
252
+ public resolveToObjects(): boolean {
253
+ let singleItemSelectionMode = false;
254
+ this.transform = Mat33.identity;
255
+
256
+ // Grow the rectangle, if necessary
257
+ if (this.region.w === 0 || this.region.h === 0) {
258
+ const padding = this.editor.viewport.visibleRect.maxDimension / 200;
259
+ this.originalRegion = Rect2.bboxOf(this.region.corners, padding);
260
+
261
+ // Only select one item if the rectangle was very small.
262
+ singleItemSelectionMode = true;
263
+ }
264
+
265
+ this.selectedElems = this.editor.image.getElementsIntersectingRegion(this.region).filter(elem => {
266
+ if (this.region.containsRect(elem.getBBox())) {
267
+ return true;
268
+ }
269
+
270
+ // Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
271
+ // As such, test with more lines than just this' edges.
272
+ const testLines = [];
273
+ for (const subregion of this.region.divideIntoGrid(2, 2)) {
274
+ testLines.push(...subregion.getEdges());
275
+ }
276
+
277
+ return testLines.some(edge => elem.intersects(edge));
278
+ });
279
+
280
+ if (singleItemSelectionMode && this.selectedElems.length > 0) {
281
+ this.selectedElems = [ this.selectedElems[this.selectedElems.length - 1] ];
282
+ }
283
+
284
+ // Find the bounding box of all selected elements.
285
+ if (!this.recomputeRegion()) {
286
+ return false;
287
+ }
288
+ this.updateUI();
289
+
290
+ return true;
291
+ }
292
+
293
+ // Recompute this' region from the selected elements.
294
+ // Returns false if the selection is empty.
295
+ public recomputeRegion(): boolean {
296
+ const newRegion = this.selectedElems.reduce((
297
+ accumulator: Rect2|null, elem: AbstractComponent
298
+ ): Rect2 => {
299
+ return (accumulator ?? elem.getBBox()).union(elem.getBBox());
300
+ }, null);
301
+
302
+ if (!newRegion) {
303
+ this.cancelSelection();
304
+ return false;
305
+ }
306
+
307
+ this.originalRegion = newRegion;
308
+
309
+ const minSize = this.getMinCanvasSize();
310
+ if (this.originalRegion.w < minSize || this.originalRegion.h < minSize) {
311
+ // Add padding
312
+ const padding = minSize / 2;
313
+ this.originalRegion = Rect2.bboxOf(
314
+ this.originalRegion.corners, padding
315
+ );
316
+ }
317
+
318
+ return true;
319
+ }
320
+
321
+ public getMinCanvasSize(): number {
322
+ const canvasHandleSize = handleSize / this.editor.viewport.getScaleFactor();
323
+ return canvasHandleSize * 2;
324
+ }
325
+
326
+ public getSelectedItemCount() {
327
+ return this.selectedElems.length;
328
+ }
329
+
330
+ // @internal
331
+ public updateUI() {
332
+ // marginLeft, marginTop: Display relative to the top left of the selection overlay.
333
+ // left, top don't work for this.
334
+ this.backgroundElem.style.marginLeft = `${this.screenRegion.topLeft.x}px`;
335
+ this.backgroundElem.style.marginTop = `${this.screenRegion.topLeft.y}px`;
336
+
337
+ this.backgroundElem.style.width = `${this.screenRegion.width}px`;
338
+ this.backgroundElem.style.height = `${this.screenRegion.height}px`;
339
+
340
+ const rotationDeg = this.screenRegionRotation * 180 / Math.PI;
341
+ this.backgroundElem.style.transform = `rotate(${rotationDeg}deg)`;
342
+ this.backgroundElem.style.transformOrigin = 'center';
343
+
344
+ for (const handle of this.handles) {
345
+ handle.updatePosition();
346
+ }
347
+ }
348
+
349
+ private targetHandle: SelectionHandle|null = null;
350
+ private backgroundDragging: boolean = false;
351
+ public onDragStart(pointer: Pointer, target: EventTarget): boolean {
352
+ for (const handle of this.handles) {
353
+ if (handle.isTarget(target)) {
354
+ handle.handleDragStart(pointer);
355
+ this.targetHandle = handle;
356
+ return true;
357
+ }
358
+ }
359
+
360
+ if (this.backgroundElem === target) {
361
+ this.backgroundDragging = true;
362
+ this.transformers.drag.onDragStart(pointer.canvasPos);
363
+ return true;
364
+ }
365
+
366
+ return false;
367
+ }
368
+
369
+ public onDragUpdate(pointer: Pointer) {
370
+ if (this.backgroundDragging) {
371
+ this.transformers.drag.onDragUpdate(pointer.canvasPos);
372
+ }
373
+
374
+ if (this.targetHandle) {
375
+ this.targetHandle.handleDragUpdate(pointer);
376
+ }
377
+
378
+ this.updateUI();
379
+ }
380
+
381
+ public onDragEnd() {
382
+ if (this.backgroundDragging) {
383
+ this.transformers.drag.onDragEnd();
384
+ }
385
+ else if (this.targetHandle) {
386
+ this.targetHandle.handleDragEnd();
387
+ }
388
+
389
+ this.backgroundDragging = false;
390
+ this.targetHandle = null;
391
+ this.updateUI();
392
+ }
393
+
394
+ public onDragCancel() {
395
+ this.backgroundDragging = false;
396
+ this.targetHandle = null;
397
+ this.setTransform(Mat33.identity);
398
+ }
399
+
400
+ // Scroll the viewport to this. Does not zoom
401
+ public scrollTo() {
402
+ if (this.selectedElems.length === 0) {
403
+ return;
404
+ }
405
+
406
+ const screenRect = new Rect2(0, 0, this.editor.display.width, this.editor.display.height);
407
+ if (!screenRect.containsPoint(this.screenRegion.center)) {
408
+ const closestPoint = screenRect.getClosestPointOnBoundaryTo(this.screenRegion.center);
409
+ const screenDelta = this.screenRegion.center.minus(closestPoint);
410
+ const delta = this.editor.viewport.screenToCanvasTransform.transformVec3(screenDelta);
411
+ this.editor.dispatchNoAnnounce(
412
+ Viewport.transformBy(Mat33.translation(delta.times(-1))), false
413
+ );
414
+ }
415
+ }
416
+
417
+ public deleteSelectedObjects(): Command {
418
+ return new Erase(this.selectedElems);
419
+ }
420
+
421
+ public duplicateSelectedObjects(): Command {
422
+ return new Duplicate(this.selectedElems);
423
+ }
424
+
425
+ public addTo(elem: HTMLElement) {
426
+ if (this.container.parentElement) {
427
+ this.container.remove();
428
+ }
429
+
430
+ elem.appendChild(this.container);
431
+ }
432
+
433
+ public setToPoint(point: Point2) {
434
+ this.originalRegion = this.originalRegion.grownToPoint(point);
435
+ this.updateUI();
436
+ }
437
+
438
+ public cancelSelection() {
439
+ if (this.container.parentElement) {
440
+ this.container.remove();
441
+ }
442
+ this.originalRegion = Rect2.empty;
443
+ }
444
+
445
+ public setSelectedObjects(objects: AbstractComponent[], bbox: Rect2) {
446
+ this.originalRegion = bbox;
447
+ this.selectedElems = objects;
448
+ this.updateUI();
449
+ }
450
+
451
+ public getSelectedObjects(): AbstractComponent[] {
452
+ return this.selectedElems;
453
+ }
454
+ }
455
+
@@ -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
  });