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.
- package/.github/ISSUE_TEMPLATE/translation.md +4 -1
- package/CHANGELOG.md +16 -0
- package/README.md +1 -3
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +15 -1
- package/dist/src/Editor.js +221 -78
- package/dist/src/EditorImage.js +4 -1
- package/dist/src/Pointer.d.ts +1 -1
- package/dist/src/Pointer.js +8 -3
- package/dist/src/SVGLoader.d.ts +4 -1
- package/dist/src/SVGLoader.js +78 -33
- package/dist/src/UndoRedoHistory.d.ts +1 -0
- package/dist/src/UndoRedoHistory.js +6 -0
- package/dist/src/Viewport.d.ts +2 -0
- package/dist/src/Viewport.js +26 -5
- package/dist/src/commands/lib.d.ts +2 -1
- package/dist/src/commands/lib.js +2 -1
- package/dist/src/commands/localization.d.ts +1 -0
- package/dist/src/commands/localization.js +1 -0
- package/dist/src/commands/uniteCommands.d.ts +4 -0
- package/dist/src/commands/uniteCommands.js +105 -0
- package/dist/src/components/AbstractComponent.d.ts +2 -0
- package/dist/src/components/AbstractComponent.js +41 -5
- package/dist/src/components/ImageComponent.d.ts +27 -0
- package/dist/src/components/ImageComponent.js +129 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +2 -2
- package/dist/src/components/lib.d.ts +4 -2
- package/dist/src/components/lib.js +4 -2
- package/dist/src/components/localization.d.ts +2 -0
- package/dist/src/components/localization.js +2 -0
- package/dist/src/language/assertions.d.ts +1 -0
- package/dist/src/language/assertions.js +5 -0
- package/dist/src/math/LineSegment2.d.ts +2 -0
- package/dist/src/math/LineSegment2.js +3 -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/localization.d.ts +3 -0
- package/dist/src/rendering/localization.js +3 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +7 -0
- package/dist/src/rendering/renderers/AbstractRenderer.js +2 -1
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +5 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +45 -20
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
- package/dist/src/toolbar/HTMLToolbar.js +5 -4
- package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
- package/dist/src/tools/BaseTool.d.ts +3 -1
- package/dist/src/tools/BaseTool.js +6 -0
- package/dist/src/tools/PasteHandler.d.ts +16 -0
- package/dist/src/tools/PasteHandler.js +144 -0
- 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 +37 -28
- package/dist/src/tools/lib.d.ts +2 -1
- package/dist/src/tools/lib.js +2 -1
- package/dist/src/tools/localization.d.ts +3 -0
- package/dist/src/tools/localization.js +3 -0
- package/dist/src/types.d.ts +14 -3
- package/dist/src/types.js +2 -0
- package/package.json +1 -1
- package/src/Editor.css +1 -0
- package/src/Editor.ts +275 -109
- package/src/EditorImage.ts +7 -1
- package/src/Pointer.ts +8 -3
- package/src/SVGLoader.ts +90 -36
- package/src/UndoRedoHistory.test.ts +33 -0
- package/src/UndoRedoHistory.ts +8 -0
- package/src/Viewport.ts +30 -6
- package/src/commands/lib.ts +2 -0
- package/src/commands/localization.ts +2 -0
- package/src/commands/uniteCommands.test.ts +23 -0
- package/src/commands/uniteCommands.ts +121 -0
- package/src/components/AbstractComponent.ts +53 -11
- package/src/components/ImageComponent.ts +149 -0
- package/src/components/Text.ts +2 -6
- package/src/components/builders/FreehandLineBuilder.ts +2 -2
- package/src/components/lib.ts +7 -2
- package/src/components/localization.ts +4 -0
- package/src/language/assertions.ts +6 -0
- package/src/math/LineSegment2.test.ts +9 -0
- package/src/math/LineSegment2.ts +5 -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/localization.ts +6 -0
- package/src/rendering/renderers/AbstractRenderer.ts +19 -2
- package/src/rendering/renderers/CanvasRenderer.ts +10 -1
- package/src/rendering/renderers/DummyRenderer.ts +6 -1
- package/src/rendering/renderers/SVGRenderer.ts +50 -21
- package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
- package/src/toolbar/HTMLToolbar.ts +5 -4
- package/src/toolbar/widgets/SelectionToolWidget.ts +1 -1
- package/src/tools/BaseTool.ts +9 -1
- package/src/tools/PasteHandler.ts +159 -0
- 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 +52 -45
- package/src/tools/lib.ts +2 -1
- package/src/tools/localization.ts +8 -0
- package/src/types.ts +17 -3
- package/dist/src/tools/SelectionTool.d.ts +0 -59
- package/dist/src/tools/SelectionTool.js +0 -589
- package/src/tools/SelectionTool.ts +0 -725
@@ -0,0 +1,144 @@
|
|
1
|
+
/**
|
2
|
+
* A tool that handles paste events.
|
3
|
+
* @packageDocumentation
|
4
|
+
*/
|
5
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
6
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
7
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
8
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
9
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
10
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
11
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
12
|
+
});
|
13
|
+
};
|
14
|
+
import { TextComponent } from '../components/lib';
|
15
|
+
import { uniteCommands } from '../commands/lib';
|
16
|
+
import SVGLoader from '../SVGLoader';
|
17
|
+
import { Mat33, Vec2 } from '../math/lib';
|
18
|
+
import BaseTool from './BaseTool';
|
19
|
+
import EditorImage from '../EditorImage';
|
20
|
+
import SelectionTool from './SelectionTool/SelectionTool';
|
21
|
+
import TextTool from './TextTool';
|
22
|
+
import Color4 from '../Color4';
|
23
|
+
import ImageComponent from '../components/ImageComponent';
|
24
|
+
import Viewport from '../Viewport';
|
25
|
+
// { @inheritDoc PasteHandler! }
|
26
|
+
export default class PasteHandler extends BaseTool {
|
27
|
+
constructor(editor) {
|
28
|
+
super(editor.notifier, editor.localization.pasteHandler);
|
29
|
+
this.editor = editor;
|
30
|
+
}
|
31
|
+
onPaste(event) {
|
32
|
+
const mime = event.mime.toLowerCase();
|
33
|
+
if (mime === 'image/svg+xml') {
|
34
|
+
void this.doSVGPaste(event.data);
|
35
|
+
return true;
|
36
|
+
}
|
37
|
+
else if (mime === 'text/plain') {
|
38
|
+
void this.doTextPaste(event.data);
|
39
|
+
return true;
|
40
|
+
}
|
41
|
+
else if (mime === 'image/png' || mime === 'image/jpeg') {
|
42
|
+
void this.doImagePaste(event.data);
|
43
|
+
return true;
|
44
|
+
}
|
45
|
+
return false;
|
46
|
+
}
|
47
|
+
addComponentsFromPaste(components) {
|
48
|
+
return __awaiter(this, void 0, void 0, function* () {
|
49
|
+
let bbox = null;
|
50
|
+
for (const component of components) {
|
51
|
+
if (bbox) {
|
52
|
+
bbox = bbox.union(component.getBBox());
|
53
|
+
}
|
54
|
+
else {
|
55
|
+
bbox = component.getBBox();
|
56
|
+
}
|
57
|
+
}
|
58
|
+
if (!bbox) {
|
59
|
+
return;
|
60
|
+
}
|
61
|
+
// Find a transform that scales/moves bbox onto the screen.
|
62
|
+
const visibleRect = this.editor.viewport.visibleRect;
|
63
|
+
const scaleRatioX = visibleRect.width / bbox.width;
|
64
|
+
const scaleRatioY = visibleRect.height / bbox.height;
|
65
|
+
let scaleRatio = scaleRatioX;
|
66
|
+
if (bbox.width * scaleRatio > visibleRect.width || bbox.height * scaleRatio > visibleRect.height) {
|
67
|
+
scaleRatio = scaleRatioY;
|
68
|
+
}
|
69
|
+
scaleRatio *= 2 / 3;
|
70
|
+
scaleRatio = Viewport.roundScaleRatio(scaleRatio);
|
71
|
+
const transfm = Mat33.translation(visibleRect.center.minus(bbox.center)).rightMul(Mat33.scaling2D(scaleRatio, bbox.center));
|
72
|
+
const commands = [];
|
73
|
+
for (const component of components) {
|
74
|
+
// To allow deserialization, we need to add first, then transform.
|
75
|
+
commands.push(EditorImage.addElement(component));
|
76
|
+
commands.push(component.transformBy(transfm));
|
77
|
+
}
|
78
|
+
const applyChunkSize = 100;
|
79
|
+
this.editor.dispatch(uniteCommands(commands, applyChunkSize), true);
|
80
|
+
for (const selectionTool of this.editor.toolController.getMatchingTools(SelectionTool)) {
|
81
|
+
selectionTool.setEnabled(true);
|
82
|
+
selectionTool.setSelection(components);
|
83
|
+
}
|
84
|
+
});
|
85
|
+
}
|
86
|
+
doSVGPaste(data) {
|
87
|
+
return __awaiter(this, void 0, void 0, function* () {
|
88
|
+
const sanitize = true;
|
89
|
+
const loader = SVGLoader.fromString(data, sanitize);
|
90
|
+
const components = [];
|
91
|
+
yield loader.start((component) => {
|
92
|
+
components.push(component);
|
93
|
+
}, (_countProcessed, _totalToProcess) => null);
|
94
|
+
yield this.addComponentsFromPaste(components);
|
95
|
+
});
|
96
|
+
}
|
97
|
+
doTextPaste(text) {
|
98
|
+
var _a, _b;
|
99
|
+
return __awaiter(this, void 0, void 0, function* () {
|
100
|
+
const textTools = this.editor.toolController.getMatchingTools(TextTool);
|
101
|
+
textTools.sort((a, b) => {
|
102
|
+
if (!a.isEnabled() && b.isEnabled()) {
|
103
|
+
return -1;
|
104
|
+
}
|
105
|
+
if (!b.isEnabled() && a.isEnabled()) {
|
106
|
+
return 1;
|
107
|
+
}
|
108
|
+
return 0;
|
109
|
+
});
|
110
|
+
const defaultTextStyle = { size: 12, fontFamily: 'sans', renderingStyle: { fill: Color4.red } };
|
111
|
+
const pastedTextStyle = (_b = (_a = textTools[0]) === null || _a === void 0 ? void 0 : _a.getTextStyle()) !== null && _b !== void 0 ? _b : defaultTextStyle;
|
112
|
+
const lines = text.split('\n');
|
113
|
+
let lastComponent = null;
|
114
|
+
const components = [];
|
115
|
+
for (const line of lines) {
|
116
|
+
let position = Vec2.zero;
|
117
|
+
if (lastComponent) {
|
118
|
+
const lineMargin = Math.floor(pastedTextStyle.size);
|
119
|
+
position = lastComponent.getBBox().bottomLeft.plus(Vec2.unitY.times(lineMargin));
|
120
|
+
}
|
121
|
+
const component = new TextComponent([line], Mat33.translation(position), pastedTextStyle);
|
122
|
+
components.push(component);
|
123
|
+
lastComponent = component;
|
124
|
+
}
|
125
|
+
if (components.length === 1) {
|
126
|
+
yield this.addComponentsFromPaste([components[0]]);
|
127
|
+
}
|
128
|
+
else {
|
129
|
+
// Wrap the existing `TextComponent`s --- dragging one component should drag all.
|
130
|
+
yield this.addComponentsFromPaste([
|
131
|
+
new TextComponent(components, Mat33.identity, pastedTextStyle)
|
132
|
+
]);
|
133
|
+
}
|
134
|
+
});
|
135
|
+
}
|
136
|
+
doImagePaste(dataURL) {
|
137
|
+
return __awaiter(this, void 0, void 0, function* () {
|
138
|
+
const image = new Image();
|
139
|
+
image.src = dataURL;
|
140
|
+
const component = yield ImageComponent.fromImage(image, Mat33.identity);
|
141
|
+
yield this.addComponentsFromPaste([component]);
|
142
|
+
});
|
143
|
+
}
|
144
|
+
}
|
package/dist/src/tools/Pen.js
CHANGED
@@ -131,7 +131,7 @@ export default class Pen extends BaseTool {
|
|
131
131
|
newThickness = this.getThickness() * 3 / 2;
|
132
132
|
}
|
133
133
|
if (newThickness !== undefined) {
|
134
|
-
newThickness = Math.min(Math.max(1, newThickness),
|
134
|
+
newThickness = Math.min(Math.max(1, newThickness), 256);
|
135
135
|
this.setThickness(newThickness);
|
136
136
|
return true;
|
137
137
|
}
|
@@ -0,0 +1,54 @@
|
|
1
|
+
/**
|
2
|
+
* @internal
|
3
|
+
* @packageDocumentation
|
4
|
+
*/
|
5
|
+
import Editor from '../../Editor';
|
6
|
+
import { Mat33, Rect2 } from '../../math/lib';
|
7
|
+
import { Point2 } from '../../math/Vec2';
|
8
|
+
import Pointer from '../../Pointer';
|
9
|
+
import AbstractComponent from '../../components/AbstractComponent';
|
10
|
+
import Command from '../../commands/Command';
|
11
|
+
export default class Selection {
|
12
|
+
private editor;
|
13
|
+
private handles;
|
14
|
+
private originalRegion;
|
15
|
+
private transformers;
|
16
|
+
private transform;
|
17
|
+
private transformCommands;
|
18
|
+
private selectedElems;
|
19
|
+
private container;
|
20
|
+
private backgroundElem;
|
21
|
+
constructor(startPoint: Point2, editor: Editor);
|
22
|
+
getTransform(): Mat33;
|
23
|
+
get preTransformRegion(): Rect2;
|
24
|
+
get region(): Rect2;
|
25
|
+
get regionRotation(): number;
|
26
|
+
get preTransformedScreenRegion(): Rect2;
|
27
|
+
get preTransformedScreenRegionRotation(): number;
|
28
|
+
get screenRegion(): Rect2;
|
29
|
+
get screenRegionRotation(): number;
|
30
|
+
private computeTransformCommands;
|
31
|
+
setTransform(transform: Mat33, preview?: boolean): void;
|
32
|
+
finalizeTransform(): void;
|
33
|
+
private static ApplyTransformationCommand;
|
34
|
+
private previewTransformCmds;
|
35
|
+
resolveToObjects(): boolean;
|
36
|
+
recomputeRegion(): boolean;
|
37
|
+
getMinCanvasSize(): number;
|
38
|
+
getSelectedItemCount(): number;
|
39
|
+
updateUI(): void;
|
40
|
+
private targetHandle;
|
41
|
+
private backgroundDragging;
|
42
|
+
onDragStart(pointer: Pointer, target: EventTarget): boolean;
|
43
|
+
onDragUpdate(pointer: Pointer): void;
|
44
|
+
onDragEnd(): void;
|
45
|
+
onDragCancel(): void;
|
46
|
+
scrollTo(): void;
|
47
|
+
deleteSelectedObjects(): Command;
|
48
|
+
duplicateSelectedObjects(): Command;
|
49
|
+
addTo(elem: HTMLElement): void;
|
50
|
+
setToPoint(point: Point2): void;
|
51
|
+
cancelSelection(): void;
|
52
|
+
setSelectedObjects(objects: AbstractComponent[], bbox: Rect2): void;
|
53
|
+
getSelectedObjects(): AbstractComponent[];
|
54
|
+
}
|
@@ -0,0 +1,337 @@
|
|
1
|
+
/**
|
2
|
+
* @internal
|
3
|
+
* @packageDocumentation
|
4
|
+
*/
|
5
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
6
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
7
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
8
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
9
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
10
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
11
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
12
|
+
});
|
13
|
+
};
|
14
|
+
var _a;
|
15
|
+
import SerializableCommand from '../../commands/SerializableCommand';
|
16
|
+
import { Mat33, Rect2 } from '../../math/lib';
|
17
|
+
import { Vec2 } from '../../math/Vec2';
|
18
|
+
import SelectionHandle, { HandleShape, handleSize } from './SelectionHandle';
|
19
|
+
import { cssPrefix } from './SelectionTool';
|
20
|
+
import Viewport from '../../Viewport';
|
21
|
+
import Erase from '../../commands/Erase';
|
22
|
+
import Duplicate from '../../commands/Duplicate';
|
23
|
+
import { DragTransformer, ResizeTransformer, RotateTransformer } from './TransformMode';
|
24
|
+
import { ResizeMode } from './types';
|
25
|
+
const updateChunkSize = 100;
|
26
|
+
// @internal
|
27
|
+
export default class Selection {
|
28
|
+
constructor(startPoint, editor) {
|
29
|
+
this.editor = editor;
|
30
|
+
this.transform = Mat33.identity;
|
31
|
+
this.transformCommands = [];
|
32
|
+
this.selectedElems = [];
|
33
|
+
this.targetHandle = null;
|
34
|
+
this.backgroundDragging = false;
|
35
|
+
this.originalRegion = new Rect2(startPoint.x, startPoint.y, 0, 0);
|
36
|
+
this.transformers = {
|
37
|
+
drag: new DragTransformer(editor, this),
|
38
|
+
resize: new ResizeTransformer(editor, this),
|
39
|
+
rotate: new RotateTransformer(editor, this),
|
40
|
+
};
|
41
|
+
this.container = document.createElement('div');
|
42
|
+
this.backgroundElem = document.createElement('div');
|
43
|
+
this.backgroundElem.classList.add(`${cssPrefix}selection-background`);
|
44
|
+
this.container.appendChild(this.backgroundElem);
|
45
|
+
const resizeHorizontalHandle = new SelectionHandle(HandleShape.Square, Vec2.of(1, 0.5), this, (startPoint) => this.transformers.resize.onDragStart(startPoint, ResizeMode.HorizontalOnly), (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint), () => this.transformers.resize.onDragEnd());
|
46
|
+
const resizeVerticalHandle = new SelectionHandle(HandleShape.Square, Vec2.of(0.5, 1), this, (startPoint) => this.transformers.resize.onDragStart(startPoint, ResizeMode.VerticalOnly), (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint), () => this.transformers.resize.onDragEnd());
|
47
|
+
const resizeBothHandle = new SelectionHandle(HandleShape.Square, Vec2.of(1, 1), this, (startPoint) => this.transformers.resize.onDragStart(startPoint, ResizeMode.Both), (currentPoint) => this.transformers.resize.onDragUpdate(currentPoint), () => this.transformers.resize.onDragEnd());
|
48
|
+
const rotationHandle = new SelectionHandle(HandleShape.Circle, Vec2.of(0.5, 0), this, (startPoint) => this.transformers.rotate.onDragStart(startPoint), (currentPoint) => this.transformers.rotate.onDragUpdate(currentPoint), () => this.transformers.rotate.onDragEnd());
|
49
|
+
this.handles = [
|
50
|
+
resizeBothHandle,
|
51
|
+
resizeHorizontalHandle,
|
52
|
+
resizeVerticalHandle,
|
53
|
+
rotationHandle,
|
54
|
+
];
|
55
|
+
for (const handle of this.handles) {
|
56
|
+
handle.addTo(this.backgroundElem);
|
57
|
+
}
|
58
|
+
}
|
59
|
+
getTransform() {
|
60
|
+
return this.transform;
|
61
|
+
}
|
62
|
+
get preTransformRegion() {
|
63
|
+
return this.originalRegion;
|
64
|
+
}
|
65
|
+
get region() {
|
66
|
+
// TODO: This currently assumes that the region rotates about its center.
|
67
|
+
// This may not be true.
|
68
|
+
const rotationMatrix = Mat33.zRotation(this.regionRotation, this.originalRegion.center);
|
69
|
+
const scaleAndTranslateMat = this.transform.rightMul(rotationMatrix.inverse());
|
70
|
+
return this.originalRegion.transformedBoundingBox(scaleAndTranslateMat);
|
71
|
+
}
|
72
|
+
get regionRotation() {
|
73
|
+
return this.transform.transformVec3(Vec2.unitX).angle();
|
74
|
+
}
|
75
|
+
get preTransformedScreenRegion() {
|
76
|
+
const toScreen = (vec) => this.editor.viewport.canvasToScreen(vec);
|
77
|
+
return Rect2.fromCorners(toScreen(this.preTransformRegion.topLeft), toScreen(this.preTransformRegion.bottomRight));
|
78
|
+
}
|
79
|
+
get preTransformedScreenRegionRotation() {
|
80
|
+
return this.editor.viewport.getRotationAngle();
|
81
|
+
}
|
82
|
+
get screenRegion() {
|
83
|
+
const toScreen = this.editor.viewport.canvasToScreenTransform;
|
84
|
+
const scaleFactor = this.editor.viewport.getScaleFactor();
|
85
|
+
const screenCenter = toScreen.transformVec2(this.region.center);
|
86
|
+
return new Rect2(screenCenter.x, screenCenter.y, scaleFactor * this.region.width, scaleFactor * this.region.height).translatedBy(this.region.size.times(-scaleFactor / 2));
|
87
|
+
}
|
88
|
+
get screenRegionRotation() {
|
89
|
+
return this.regionRotation + this.editor.viewport.getRotationAngle();
|
90
|
+
}
|
91
|
+
computeTransformCommands() {
|
92
|
+
return this.selectedElems.map(elem => {
|
93
|
+
return elem.transformBy(this.transform);
|
94
|
+
});
|
95
|
+
}
|
96
|
+
// Applies, previews, but doesn't finalize the given transformation.
|
97
|
+
setTransform(transform, preview = true) {
|
98
|
+
this.transform = transform;
|
99
|
+
if (preview) {
|
100
|
+
this.previewTransformCmds();
|
101
|
+
this.scrollTo();
|
102
|
+
}
|
103
|
+
}
|
104
|
+
// Applies the current transformation to the selection
|
105
|
+
finalizeTransform() {
|
106
|
+
this.transformCommands.forEach(cmd => {
|
107
|
+
cmd.unapply(this.editor);
|
108
|
+
});
|
109
|
+
const fullTransform = this.transform;
|
110
|
+
const currentTransfmCommands = this.computeTransformCommands();
|
111
|
+
// Reset for the next drag
|
112
|
+
this.transformCommands = [];
|
113
|
+
this.originalRegion = this.originalRegion.transformedBoundingBox(this.transform);
|
114
|
+
this.transform = Mat33.identity;
|
115
|
+
// Make the commands undo-able
|
116
|
+
this.editor.dispatch(new Selection.ApplyTransformationCommand(this, currentTransfmCommands, fullTransform));
|
117
|
+
}
|
118
|
+
// Preview the effects of the current transformation on the selection
|
119
|
+
previewTransformCmds() {
|
120
|
+
// Don't render what we're moving if it's likely to be slow.
|
121
|
+
if (this.selectedElems.length > updateChunkSize) {
|
122
|
+
this.updateUI();
|
123
|
+
return;
|
124
|
+
}
|
125
|
+
this.transformCommands.forEach(cmd => cmd.unapply(this.editor));
|
126
|
+
this.transformCommands = this.computeTransformCommands();
|
127
|
+
this.transformCommands.forEach(cmd => cmd.apply(this.editor));
|
128
|
+
this.updateUI();
|
129
|
+
}
|
130
|
+
// Find the objects corresponding to this in the document,
|
131
|
+
// select them.
|
132
|
+
// Returns false iff nothing was selected.
|
133
|
+
resolveToObjects() {
|
134
|
+
let singleItemSelectionMode = false;
|
135
|
+
this.transform = Mat33.identity;
|
136
|
+
// Grow the rectangle, if necessary
|
137
|
+
if (this.region.w === 0 || this.region.h === 0) {
|
138
|
+
const padding = this.editor.viewport.visibleRect.maxDimension / 200;
|
139
|
+
this.originalRegion = Rect2.bboxOf(this.region.corners, padding);
|
140
|
+
// Only select one item if the rectangle was very small.
|
141
|
+
singleItemSelectionMode = true;
|
142
|
+
}
|
143
|
+
this.selectedElems = this.editor.image.getElementsIntersectingRegion(this.region).filter(elem => {
|
144
|
+
if (this.region.containsRect(elem.getBBox())) {
|
145
|
+
return true;
|
146
|
+
}
|
147
|
+
// Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
|
148
|
+
// As such, test with more lines than just this' edges.
|
149
|
+
const testLines = [];
|
150
|
+
for (const subregion of this.region.divideIntoGrid(2, 2)) {
|
151
|
+
testLines.push(...subregion.getEdges());
|
152
|
+
}
|
153
|
+
return testLines.some(edge => elem.intersects(edge));
|
154
|
+
});
|
155
|
+
if (singleItemSelectionMode && this.selectedElems.length > 0) {
|
156
|
+
this.selectedElems = [this.selectedElems[this.selectedElems.length - 1]];
|
157
|
+
}
|
158
|
+
// Find the bounding box of all selected elements.
|
159
|
+
if (!this.recomputeRegion()) {
|
160
|
+
return false;
|
161
|
+
}
|
162
|
+
this.updateUI();
|
163
|
+
return true;
|
164
|
+
}
|
165
|
+
// Recompute this' region from the selected elements.
|
166
|
+
// Returns false if the selection is empty.
|
167
|
+
recomputeRegion() {
|
168
|
+
const newRegion = this.selectedElems.reduce((accumulator, elem) => {
|
169
|
+
return (accumulator !== null && accumulator !== void 0 ? accumulator : elem.getBBox()).union(elem.getBBox());
|
170
|
+
}, null);
|
171
|
+
if (!newRegion) {
|
172
|
+
this.cancelSelection();
|
173
|
+
return false;
|
174
|
+
}
|
175
|
+
this.originalRegion = newRegion;
|
176
|
+
const minSize = this.getMinCanvasSize();
|
177
|
+
if (this.originalRegion.w < minSize || this.originalRegion.h < minSize) {
|
178
|
+
// Add padding
|
179
|
+
const padding = minSize / 2;
|
180
|
+
this.originalRegion = Rect2.bboxOf(this.originalRegion.corners, padding);
|
181
|
+
}
|
182
|
+
return true;
|
183
|
+
}
|
184
|
+
getMinCanvasSize() {
|
185
|
+
const canvasHandleSize = handleSize / this.editor.viewport.getScaleFactor();
|
186
|
+
return canvasHandleSize * 2;
|
187
|
+
}
|
188
|
+
getSelectedItemCount() {
|
189
|
+
return this.selectedElems.length;
|
190
|
+
}
|
191
|
+
// @internal
|
192
|
+
updateUI() {
|
193
|
+
// marginLeft, marginTop: Display relative to the top left of the selection overlay.
|
194
|
+
// left, top don't work for this.
|
195
|
+
this.backgroundElem.style.marginLeft = `${this.screenRegion.topLeft.x}px`;
|
196
|
+
this.backgroundElem.style.marginTop = `${this.screenRegion.topLeft.y}px`;
|
197
|
+
this.backgroundElem.style.width = `${this.screenRegion.width}px`;
|
198
|
+
this.backgroundElem.style.height = `${this.screenRegion.height}px`;
|
199
|
+
const rotationDeg = this.screenRegionRotation * 180 / Math.PI;
|
200
|
+
this.backgroundElem.style.transform = `rotate(${rotationDeg}deg)`;
|
201
|
+
this.backgroundElem.style.transformOrigin = 'center';
|
202
|
+
for (const handle of this.handles) {
|
203
|
+
handle.updatePosition();
|
204
|
+
}
|
205
|
+
}
|
206
|
+
onDragStart(pointer, target) {
|
207
|
+
for (const handle of this.handles) {
|
208
|
+
if (handle.isTarget(target)) {
|
209
|
+
handle.handleDragStart(pointer);
|
210
|
+
this.targetHandle = handle;
|
211
|
+
return true;
|
212
|
+
}
|
213
|
+
}
|
214
|
+
if (this.backgroundElem === target) {
|
215
|
+
this.backgroundDragging = true;
|
216
|
+
this.transformers.drag.onDragStart(pointer.canvasPos);
|
217
|
+
return true;
|
218
|
+
}
|
219
|
+
return false;
|
220
|
+
}
|
221
|
+
onDragUpdate(pointer) {
|
222
|
+
if (this.backgroundDragging) {
|
223
|
+
this.transformers.drag.onDragUpdate(pointer.canvasPos);
|
224
|
+
}
|
225
|
+
if (this.targetHandle) {
|
226
|
+
this.targetHandle.handleDragUpdate(pointer);
|
227
|
+
}
|
228
|
+
this.updateUI();
|
229
|
+
}
|
230
|
+
onDragEnd() {
|
231
|
+
if (this.backgroundDragging) {
|
232
|
+
this.transformers.drag.onDragEnd();
|
233
|
+
}
|
234
|
+
else if (this.targetHandle) {
|
235
|
+
this.targetHandle.handleDragEnd();
|
236
|
+
}
|
237
|
+
this.backgroundDragging = false;
|
238
|
+
this.targetHandle = null;
|
239
|
+
this.updateUI();
|
240
|
+
}
|
241
|
+
onDragCancel() {
|
242
|
+
this.backgroundDragging = false;
|
243
|
+
this.targetHandle = null;
|
244
|
+
this.setTransform(Mat33.identity);
|
245
|
+
}
|
246
|
+
// Scroll the viewport to this. Does not zoom
|
247
|
+
scrollTo() {
|
248
|
+
if (this.selectedElems.length === 0) {
|
249
|
+
return;
|
250
|
+
}
|
251
|
+
const screenRect = new Rect2(0, 0, this.editor.display.width, this.editor.display.height);
|
252
|
+
if (!screenRect.containsPoint(this.screenRegion.center)) {
|
253
|
+
const closestPoint = screenRect.getClosestPointOnBoundaryTo(this.screenRegion.center);
|
254
|
+
const screenDelta = this.screenRegion.center.minus(closestPoint);
|
255
|
+
const delta = this.editor.viewport.screenToCanvasTransform.transformVec3(screenDelta);
|
256
|
+
this.editor.dispatchNoAnnounce(Viewport.transformBy(Mat33.translation(delta.times(-1))), false);
|
257
|
+
}
|
258
|
+
}
|
259
|
+
deleteSelectedObjects() {
|
260
|
+
return new Erase(this.selectedElems);
|
261
|
+
}
|
262
|
+
duplicateSelectedObjects() {
|
263
|
+
return new Duplicate(this.selectedElems);
|
264
|
+
}
|
265
|
+
addTo(elem) {
|
266
|
+
if (this.container.parentElement) {
|
267
|
+
this.container.remove();
|
268
|
+
}
|
269
|
+
elem.appendChild(this.container);
|
270
|
+
}
|
271
|
+
setToPoint(point) {
|
272
|
+
this.originalRegion = this.originalRegion.grownToPoint(point);
|
273
|
+
this.updateUI();
|
274
|
+
}
|
275
|
+
cancelSelection() {
|
276
|
+
if (this.container.parentElement) {
|
277
|
+
this.container.remove();
|
278
|
+
}
|
279
|
+
this.originalRegion = Rect2.empty;
|
280
|
+
}
|
281
|
+
setSelectedObjects(objects, bbox) {
|
282
|
+
this.originalRegion = bbox;
|
283
|
+
this.selectedElems = objects;
|
284
|
+
this.updateUI();
|
285
|
+
}
|
286
|
+
getSelectedObjects() {
|
287
|
+
return this.selectedElems;
|
288
|
+
}
|
289
|
+
}
|
290
|
+
_a = Selection;
|
291
|
+
(() => {
|
292
|
+
SerializableCommand.register('selection-tool-transform', (json, editor) => {
|
293
|
+
// The selection box is lost when serializing/deserializing. No need to store box rotation
|
294
|
+
const fullTransform = new Mat33(...json.transform);
|
295
|
+
const commands = json.commands.map(data => SerializableCommand.deserialize(data, editor));
|
296
|
+
return new _a.ApplyTransformationCommand(null, commands, fullTransform);
|
297
|
+
});
|
298
|
+
})();
|
299
|
+
Selection.ApplyTransformationCommand = class extends SerializableCommand {
|
300
|
+
constructor(selection, currentTransfmCommands, fullTransform) {
|
301
|
+
super('selection-tool-transform');
|
302
|
+
this.selection = selection;
|
303
|
+
this.currentTransfmCommands = currentTransfmCommands;
|
304
|
+
this.fullTransform = fullTransform;
|
305
|
+
}
|
306
|
+
apply(editor) {
|
307
|
+
var _b, _c, _d, _e, _f;
|
308
|
+
return __awaiter(this, void 0, void 0, function* () {
|
309
|
+
(_b = this.selection) === null || _b === void 0 ? void 0 : _b.setTransform(this.fullTransform, false);
|
310
|
+
(_c = this.selection) === null || _c === void 0 ? void 0 : _c.updateUI();
|
311
|
+
yield editor.asyncApplyCommands(this.currentTransfmCommands, updateChunkSize);
|
312
|
+
(_d = this.selection) === null || _d === void 0 ? void 0 : _d.setTransform(Mat33.identity, false);
|
313
|
+
(_e = this.selection) === null || _e === void 0 ? void 0 : _e.recomputeRegion();
|
314
|
+
(_f = this.selection) === null || _f === void 0 ? void 0 : _f.updateUI();
|
315
|
+
});
|
316
|
+
}
|
317
|
+
unapply(editor) {
|
318
|
+
var _b, _c, _d, _e, _f;
|
319
|
+
return __awaiter(this, void 0, void 0, function* () {
|
320
|
+
(_b = this.selection) === null || _b === void 0 ? void 0 : _b.setTransform(this.fullTransform.inverse(), false);
|
321
|
+
(_c = this.selection) === null || _c === void 0 ? void 0 : _c.updateUI();
|
322
|
+
yield editor.asyncUnapplyCommands(this.currentTransfmCommands, updateChunkSize);
|
323
|
+
(_d = this.selection) === null || _d === void 0 ? void 0 : _d.setTransform(Mat33.identity);
|
324
|
+
(_e = this.selection) === null || _e === void 0 ? void 0 : _e.recomputeRegion();
|
325
|
+
(_f = this.selection) === null || _f === void 0 ? void 0 : _f.updateUI();
|
326
|
+
});
|
327
|
+
}
|
328
|
+
serializeToJSON() {
|
329
|
+
return {
|
330
|
+
commands: this.currentTransfmCommands.map(command => command.serialize()),
|
331
|
+
transform: this.fullTransform.toArray(),
|
332
|
+
};
|
333
|
+
}
|
334
|
+
description(_editor, localizationTable) {
|
335
|
+
return localizationTable.transformedElements(this.currentTransfmCommands.length);
|
336
|
+
}
|
337
|
+
};
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import { Point2, Vec2 } from '../../math/Vec2';
|
2
|
+
import Selection from './Selection';
|
3
|
+
import Pointer from '../../Pointer';
|
4
|
+
export declare enum HandleShape {
|
5
|
+
Circle = 0,
|
6
|
+
Square = 1
|
7
|
+
}
|
8
|
+
export declare const handleSize = 30;
|
9
|
+
export declare type DragStartCallback = (startPoint: Point2) => void;
|
10
|
+
export declare type DragUpdateCallback = (canvasPoint: Point2) => void;
|
11
|
+
export declare type DragEndCallback = () => void;
|
12
|
+
export default class SelectionHandle {
|
13
|
+
readonly shape: HandleShape;
|
14
|
+
private readonly parentSide;
|
15
|
+
private readonly parent;
|
16
|
+
private readonly onDragStart;
|
17
|
+
private readonly onDragUpdate;
|
18
|
+
private readonly onDragEnd;
|
19
|
+
private element;
|
20
|
+
constructor(shape: HandleShape, parentSide: Vec2, parent: Selection, onDragStart: DragStartCallback, onDragUpdate: DragUpdateCallback, onDragEnd: DragEndCallback);
|
21
|
+
/**
|
22
|
+
* Adds this to `container`, where `conatiner` should be the background/selection
|
23
|
+
* element visible on the screen.
|
24
|
+
*/
|
25
|
+
addTo(container: HTMLElement): void;
|
26
|
+
updatePosition(): void;
|
27
|
+
/**
|
28
|
+
* @returns `true` if the given `EventTarget` matches this.
|
29
|
+
*/
|
30
|
+
isTarget(target: EventTarget): boolean;
|
31
|
+
private dragLastPos;
|
32
|
+
handleDragStart(pointer: Pointer): void;
|
33
|
+
handleDragUpdate(pointer: Pointer): void;
|
34
|
+
handleDragEnd(): void;
|
35
|
+
}
|
@@ -0,0 +1,75 @@
|
|
1
|
+
import { assertUnreachable } from '../../language/assertions';
|
2
|
+
import { Vec2 } from '../../math/Vec2';
|
3
|
+
import { cssPrefix } from './SelectionTool';
|
4
|
+
export var HandleShape;
|
5
|
+
(function (HandleShape) {
|
6
|
+
HandleShape[HandleShape["Circle"] = 0] = "Circle";
|
7
|
+
HandleShape[HandleShape["Square"] = 1] = "Square";
|
8
|
+
})(HandleShape || (HandleShape = {}));
|
9
|
+
export const handleSize = 30;
|
10
|
+
export default class SelectionHandle {
|
11
|
+
// Bounding box in screen coordinates.
|
12
|
+
constructor(shape, parentSide, parent, onDragStart, onDragUpdate, onDragEnd) {
|
13
|
+
this.shape = shape;
|
14
|
+
this.parentSide = parentSide;
|
15
|
+
this.parent = parent;
|
16
|
+
this.onDragStart = onDragStart;
|
17
|
+
this.onDragUpdate = onDragUpdate;
|
18
|
+
this.onDragEnd = onDragEnd;
|
19
|
+
this.dragLastPos = null;
|
20
|
+
this.element = document.createElement('div');
|
21
|
+
this.element.classList.add(`${cssPrefix}handle`);
|
22
|
+
switch (shape) {
|
23
|
+
case HandleShape.Circle:
|
24
|
+
this.element.classList.add(`${cssPrefix}circle`);
|
25
|
+
break;
|
26
|
+
case HandleShape.Square:
|
27
|
+
this.element.classList.add(`${cssPrefix}square`);
|
28
|
+
break;
|
29
|
+
default:
|
30
|
+
assertUnreachable(shape);
|
31
|
+
}
|
32
|
+
this.updatePosition();
|
33
|
+
}
|
34
|
+
/**
|
35
|
+
* Adds this to `container`, where `conatiner` should be the background/selection
|
36
|
+
* element visible on the screen.
|
37
|
+
*/
|
38
|
+
addTo(container) {
|
39
|
+
container.appendChild(this.element);
|
40
|
+
}
|
41
|
+
updatePosition() {
|
42
|
+
const parentRect = this.parent.screenRegion;
|
43
|
+
const size = Vec2.of(handleSize, handleSize);
|
44
|
+
const topLeft = parentRect.size.scale(this.parentSide)
|
45
|
+
// Center
|
46
|
+
.minus(size.times(1 / 2));
|
47
|
+
// Position within the selection box.
|
48
|
+
this.element.style.marginLeft = `${topLeft.x}px`;
|
49
|
+
this.element.style.marginTop = `${topLeft.y}px`;
|
50
|
+
this.element.style.width = `${size.x}px`;
|
51
|
+
this.element.style.height = `${size.y}px`;
|
52
|
+
}
|
53
|
+
/**
|
54
|
+
* @returns `true` if the given `EventTarget` matches this.
|
55
|
+
*/
|
56
|
+
isTarget(target) {
|
57
|
+
return target === this.element;
|
58
|
+
}
|
59
|
+
handleDragStart(pointer) {
|
60
|
+
this.onDragStart(pointer.canvasPos);
|
61
|
+
this.dragLastPos = pointer.canvasPos;
|
62
|
+
}
|
63
|
+
handleDragUpdate(pointer) {
|
64
|
+
if (!this.dragLastPos) {
|
65
|
+
return;
|
66
|
+
}
|
67
|
+
this.onDragUpdate(pointer.canvasPos);
|
68
|
+
}
|
69
|
+
handleDragEnd() {
|
70
|
+
if (!this.dragLastPos) {
|
71
|
+
return;
|
72
|
+
}
|
73
|
+
this.onDragEnd();
|
74
|
+
}
|
75
|
+
}
|