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.
- package/CHANGELOG.md +9 -1
- package/README.md +1 -3
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +11 -0
- package/dist/src/Editor.js +104 -76
- package/dist/src/Pointer.d.ts +1 -1
- package/dist/src/Pointer.js +8 -3
- package/dist/src/Viewport.d.ts +1 -0
- package/dist/src/Viewport.js +14 -1
- package/dist/src/components/ImageComponent.d.ts +2 -2
- package/dist/src/language/assertions.d.ts +1 -0
- package/dist/src/language/assertions.js +5 -0
- package/dist/src/math/Mat33.d.ts +38 -2
- package/dist/src/math/Mat33.js +30 -1
- package/dist/src/math/Path.d.ts +1 -1
- package/dist/src/math/Path.js +10 -8
- package/dist/src/math/Vec3.d.ts +11 -1
- package/dist/src/math/Vec3.js +15 -0
- package/dist/src/math/rounding.d.ts +1 -0
- package/dist/src/math/rounding.js +13 -6
- package/dist/src/rendering/renderers/AbstractRenderer.js +2 -1
- package/dist/src/toolbar/HTMLToolbar.js +5 -4
- package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
- package/dist/src/tools/PasteHandler.js +3 -1
- package/dist/src/tools/Pen.js +1 -1
- package/dist/src/tools/SelectionTool/Selection.d.ts +54 -0
- package/dist/src/tools/SelectionTool/Selection.js +337 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +35 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.js +75 -0
- package/dist/src/tools/SelectionTool/SelectionTool.d.ts +31 -0
- package/dist/src/tools/SelectionTool/SelectionTool.js +276 -0
- package/dist/src/tools/SelectionTool/TransformMode.d.ts +34 -0
- package/dist/src/tools/SelectionTool/TransformMode.js +98 -0
- package/dist/src/tools/SelectionTool/types.d.ts +9 -0
- package/dist/src/tools/SelectionTool/types.js +11 -0
- package/dist/src/tools/ToolController.js +1 -1
- package/dist/src/tools/lib.d.ts +1 -1
- package/dist/src/tools/lib.js +1 -1
- package/dist/src/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/Editor.css +1 -0
- package/src/Editor.ts +145 -108
- package/src/Pointer.ts +8 -3
- package/src/Viewport.ts +17 -2
- package/src/components/AbstractComponent.ts +2 -6
- package/src/components/ImageComponent.ts +2 -6
- package/src/components/Text.ts +2 -6
- package/src/language/assertions.ts +6 -0
- package/src/math/Mat33.test.ts +14 -0
- package/src/math/Mat33.ts +43 -2
- package/src/math/Path.toString.test.ts +12 -1
- package/src/math/Path.ts +11 -9
- package/src/math/Vec3.ts +22 -1
- package/src/math/rounding.test.ts +30 -5
- package/src/math/rounding.ts +16 -7
- package/src/rendering/renderers/AbstractRenderer.ts +3 -2
- package/src/toolbar/HTMLToolbar.ts +5 -4
- package/src/toolbar/widgets/SelectionToolWidget.ts +1 -1
- package/src/tools/PasteHandler.ts +4 -1
- package/src/tools/Pen.ts +1 -1
- package/src/tools/SelectionTool/Selection.ts +455 -0
- package/src/tools/SelectionTool/SelectionHandle.ts +99 -0
- package/src/tools/SelectionTool/SelectionTool.css +22 -0
- package/src/tools/{SelectionTool.test.ts → SelectionTool/SelectionTool.test.ts} +21 -21
- package/src/tools/SelectionTool/SelectionTool.ts +335 -0
- package/src/tools/SelectionTool/TransformMode.ts +114 -0
- package/src/tools/SelectionTool/types.ts +11 -0
- package/src/tools/ToolController.ts +1 -1
- package/src/tools/lib.ts +1 -1
- package/src/types.ts +1 -1
- package/dist/src/tools/SelectionTool.d.ts +0 -65
- package/dist/src/tools/SelectionTool.js +0 -647
- package/src/tools/SelectionTool.ts +0 -797
package/src/math/Vec3.ts
CHANGED
@@ -95,6 +95,27 @@ export default class Vec3 {
|
|
95
95
|
);
|
96
96
|
}
|
97
97
|
|
98
|
+
/**
|
99
|
+
* If `other` is a `Vec3`, multiplies `this` component-wise by `other`. Otherwise,
|
100
|
+
* if `other is a `number`, returns the result of scalar multiplication.
|
101
|
+
*
|
102
|
+
* @example
|
103
|
+
* ```
|
104
|
+
* Vec3.of(1, 2, 3).scale(Vec3.of(2, 4, 6)); // → Vec3(2, 8, 18)
|
105
|
+
* ```
|
106
|
+
*/
|
107
|
+
public scale(other: Vec3|number): Vec3 {
|
108
|
+
if (typeof other === 'number') {
|
109
|
+
return this.times(other);
|
110
|
+
}
|
111
|
+
|
112
|
+
return Vec3.of(
|
113
|
+
this.x * other.x,
|
114
|
+
this.y * other.y,
|
115
|
+
this.z * other.z,
|
116
|
+
);
|
117
|
+
}
|
118
|
+
|
98
119
|
/**
|
99
120
|
* Returns a vector orthogonal to this. If this is a Vec2, returns `this` rotated
|
100
121
|
* 90 degrees counter-clockwise.
|
@@ -158,7 +179,7 @@ export default class Vec3 {
|
|
158
179
|
);
|
159
180
|
}
|
160
181
|
|
161
|
-
public asArray(): number
|
182
|
+
public asArray(): [ number, number, number ] {
|
162
183
|
return [this.x, this.y, this.z];
|
163
184
|
}
|
164
185
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { toRoundedString, toStringOfSamePrecision } from './rounding';
|
1
|
+
import { cleanUpNumber, toRoundedString, toStringOfSamePrecision } from './rounding';
|
2
2
|
|
3
3
|
describe('toRoundedString', () => {
|
4
4
|
it('should round up numbers endings similar to .999999999999999', () => {
|
@@ -12,18 +12,28 @@ describe('toRoundedString', () => {
|
|
12
12
|
expect(toRoundedString(10.999999998)).toBe('11');
|
13
13
|
});
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
it('should round strings with multiple digits after the ending decimal points', () => {
|
16
|
+
expect(toRoundedString(292.2 - 292.8)).toBe('-.6');
|
17
|
+
expect(toRoundedString(4.06425600000023)).toBe('4.064256');
|
18
|
+
});
|
19
19
|
|
20
20
|
it('should round down strings ending endings similar to .00000001', () => {
|
21
21
|
expect(toRoundedString(10.00000001)).toBe('10');
|
22
|
+
expect(toRoundedString(-30.00000001)).toBe('-30');
|
23
|
+
expect(toRoundedString(-14.20000000000002)).toBe('-14.2');
|
24
|
+
});
|
25
|
+
|
26
|
+
it('should not round numbers insufficiently close to the next', () => {
|
27
|
+
expect(toRoundedString(-10.9999)).toBe('-10.9999');
|
28
|
+
expect(toRoundedString(-10.0001)).toBe('-10.0001');
|
29
|
+
expect(toRoundedString(-10.123499)).toBe('-10.123499');
|
30
|
+
expect(toRoundedString(0.00123499)).toBe('.00123499');
|
22
31
|
});
|
23
32
|
});
|
24
33
|
|
25
34
|
it('toStringOfSamePrecision', () => {
|
26
35
|
expect(toStringOfSamePrecision(1.23456, '1.12')).toBe('1.23');
|
36
|
+
expect(toStringOfSamePrecision(1.23456, '1.120')).toBe('1.235');
|
27
37
|
expect(toStringOfSamePrecision(1.23456, '1.1')).toBe('1.2');
|
28
38
|
expect(toStringOfSamePrecision(1.23456, '1.1', '5.32')).toBe('1.23');
|
29
39
|
expect(toStringOfSamePrecision(-1.23456, '1.1', '5.32')).toBe('-1.23');
|
@@ -37,4 +47,19 @@ it('toStringOfSamePrecision', () => {
|
|
37
47
|
expect(toStringOfSamePrecision(-0.09999999999999432, '291.3')).toBe('-.1');
|
38
48
|
expect(toStringOfSamePrecision(-0.9999999999999432, '291.3')).toBe('-1');
|
39
49
|
expect(toStringOfSamePrecision(9998.9, '.1', '-11')).toBe('9998.9');
|
50
|
+
expect(toStringOfSamePrecision(-14.20000000000002, '.000001', '-11')).toBe('-14.2');
|
51
|
+
});
|
52
|
+
|
53
|
+
it('cleanUpNumber', () => {
|
54
|
+
expect(cleanUpNumber('000.0000')).toBe('0');
|
55
|
+
expect(cleanUpNumber('-000.0000')).toBe('0');
|
56
|
+
expect(cleanUpNumber('0.0000')).toBe('0');
|
57
|
+
expect(cleanUpNumber('0.001')).toBe('.001');
|
58
|
+
expect(cleanUpNumber('-0.001')).toBe('-.001');
|
59
|
+
expect(cleanUpNumber('-0.000000001')).toBe('-.000000001');
|
60
|
+
expect(cleanUpNumber('-0.00000000100')).toBe('-.000000001');
|
61
|
+
expect(cleanUpNumber('1234')).toBe('1234');
|
62
|
+
expect(cleanUpNumber('1234.5')).toBe('1234.5');
|
63
|
+
expect(cleanUpNumber('1234.500')).toBe('1234.5');
|
64
|
+
expect(cleanUpNumber('1.1368683772161603e-13')).toBe('0');
|
40
65
|
});
|
package/src/math/rounding.ts
CHANGED
@@ -1,9 +1,17 @@
|
|
1
1
|
// @packageDocumentation @internal
|
2
2
|
|
3
3
|
// Clean up stringified numbers
|
4
|
-
const cleanUpNumber = (text: string) => {
|
4
|
+
export const cleanUpNumber = (text: string) => {
|
5
5
|
// Regular expression substitions can be somewhat expensive. Only do them
|
6
6
|
// if necessary.
|
7
|
+
|
8
|
+
if (text.indexOf('e') > 0) {
|
9
|
+
// Round to zero.
|
10
|
+
if (text.match(/[eE][-]\d{2,}$/)) {
|
11
|
+
return '0';
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
7
15
|
const lastChar = text.charAt(text.length - 1);
|
8
16
|
if (lastChar === '0' || lastChar === '.') {
|
9
17
|
// Remove trailing zeroes
|
@@ -12,10 +20,6 @@ const cleanUpNumber = (text: string) => {
|
|
12
20
|
|
13
21
|
// Remove trailing period
|
14
22
|
text = text.replace(/[.]$/, '');
|
15
|
-
|
16
|
-
if (text === '-0') {
|
17
|
-
return '0';
|
18
|
-
}
|
19
23
|
}
|
20
24
|
|
21
25
|
const firstChar = text.charAt(0);
|
@@ -23,6 +27,11 @@ const cleanUpNumber = (text: string) => {
|
|
23
27
|
// Remove unnecessary leading zeroes.
|
24
28
|
text = text.replace(/^(0+)[.]/, '.');
|
25
29
|
text = text.replace(/^-(0+)[.]/, '-.');
|
30
|
+
text = text.replace(/^(-?)0+$/, '$10');
|
31
|
+
}
|
32
|
+
|
33
|
+
if (text === '-0') {
|
34
|
+
return '0';
|
26
35
|
}
|
27
36
|
|
28
37
|
return text;
|
@@ -31,8 +40,8 @@ const cleanUpNumber = (text: string) => {
|
|
31
40
|
export const toRoundedString = (num: number): string => {
|
32
41
|
// Try to remove rounding errors. If the number ends in at least three/four zeroes
|
33
42
|
// (or nines) just one or two digits, it's probably a rounding error.
|
34
|
-
const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,
|
35
|
-
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,
|
43
|
+
const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,4}$/;
|
44
|
+
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,4}$/;
|
36
45
|
|
37
46
|
let text = num.toString(10);
|
38
47
|
if (text.indexOf('.') === -1) {
|
@@ -122,8 +122,9 @@ export default abstract class AbstractRenderer {
|
|
122
122
|
}
|
123
123
|
}
|
124
124
|
|
125
|
-
// Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill]
|
126
|
-
|
125
|
+
// Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill].
|
126
|
+
// This is equivalent to `drawPath(Path.fromRect(...).toRenderable(...))`.
|
127
|
+
public drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle) {
|
127
128
|
const path = Path.fromRect(rect, lineWidth);
|
128
129
|
this.drawPath(path.toRenderable(lineFill));
|
129
130
|
}
|
@@ -3,19 +3,20 @@ import { EditorEventType } from '../types';
|
|
3
3
|
|
4
4
|
import { coloris, init as colorisInit } from '@melloware/coloris';
|
5
5
|
import Color4 from '../Color4';
|
6
|
-
import SelectionTool from '../tools/SelectionTool';
|
7
6
|
import { defaultToolbarLocalization, ToolbarLocalization } from './localization';
|
8
7
|
import { ActionButtonIcon } from './types';
|
9
8
|
import { makeRedoIcon, makeUndoIcon } from './icons';
|
10
|
-
import
|
9
|
+
import SelectionTool from '../tools/SelectionTool/SelectionTool';
|
10
|
+
import PanZoomTool from '../tools/PanZoom';
|
11
11
|
import TextTool from '../tools/TextTool';
|
12
|
+
import EraserTool from '../tools/Eraser';
|
13
|
+
import PenTool from '../tools/Pen';
|
12
14
|
import PenToolWidget from './widgets/PenToolWidget';
|
13
15
|
import EraserWidget from './widgets/EraserToolWidget';
|
14
16
|
import SelectionToolWidget from './widgets/SelectionToolWidget';
|
15
17
|
import TextToolWidget from './widgets/TextToolWidget';
|
16
18
|
import HandToolWidget from './widgets/HandToolWidget';
|
17
19
|
import BaseWidget from './widgets/BaseWidget';
|
18
|
-
import { EraserTool, PenTool } from '../tools/lib';
|
19
20
|
|
20
21
|
export const toolbarCSSPrefix = 'toolbar-';
|
21
22
|
|
@@ -200,7 +201,7 @@ export default class HTMLToolbar {
|
|
200
201
|
this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
|
201
202
|
}
|
202
203
|
|
203
|
-
const panZoomTool = toolController.getMatchingTools(
|
204
|
+
const panZoomTool = toolController.getMatchingTools(PanZoomTool)[0];
|
204
205
|
if (panZoomTool) {
|
205
206
|
this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));
|
206
207
|
}
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import Editor from '../../Editor';
|
2
|
-
import SelectionTool from '../../tools/SelectionTool';
|
2
|
+
import SelectionTool from '../../tools/SelectionTool/SelectionTool';
|
3
3
|
import { EditorEventType } from '../../types';
|
4
4
|
import { makeDeleteSelectionIcon, makeDuplicateSelectionIcon, makeResizeViewportIcon, makeSelectionIcon } from '../icons';
|
5
5
|
import { ToolbarLocalization } from '../localization';
|
@@ -11,11 +11,12 @@ import { PasteEvent } from '../types';
|
|
11
11
|
import { Mat33, Rect2, Vec2 } from '../math/lib';
|
12
12
|
import BaseTool from './BaseTool';
|
13
13
|
import EditorImage from '../EditorImage';
|
14
|
-
import SelectionTool from './SelectionTool';
|
14
|
+
import SelectionTool from './SelectionTool/SelectionTool';
|
15
15
|
import TextTool from './TextTool';
|
16
16
|
import Color4 from '../Color4';
|
17
17
|
import { TextStyle } from '../components/Text';
|
18
18
|
import ImageComponent from '../components/ImageComponent';
|
19
|
+
import Viewport from '../Viewport';
|
19
20
|
|
20
21
|
// { @inheritDoc PasteHandler! }
|
21
22
|
export default class PasteHandler extends BaseTool {
|
@@ -67,6 +68,8 @@ export default class PasteHandler extends BaseTool {
|
|
67
68
|
}
|
68
69
|
scaleRatio *= 2 / 3;
|
69
70
|
|
71
|
+
scaleRatio = Viewport.roundScaleRatio(scaleRatio);
|
72
|
+
|
70
73
|
const transfm = Mat33.translation(
|
71
74
|
visibleRect.center.minus(bbox.center)
|
72
75
|
).rightMul(
|
package/src/tools/Pen.ts
CHANGED
@@ -171,7 +171,7 @@ export default class Pen extends BaseTool {
|
|
171
171
|
}
|
172
172
|
|
173
173
|
if (newThickness !== undefined) {
|
174
|
-
newThickness = Math.min(Math.max(1, newThickness),
|
174
|
+
newThickness = Math.min(Math.max(1, newThickness), 256);
|
175
175
|
this.setThickness(newThickness);
|
176
176
|
return true;
|
177
177
|
}
|
@@ -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
|
+
|