js-draw 0.5.0 → 0.7.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/.firebase/hosting.ZG9jcw.cache +338 -0
- package/.github/ISSUE_TEMPLATE/translation.md +1 -1
- package/CHANGELOG.md +19 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +8 -6
- package/dist/src/Editor.js +8 -4
- package/dist/src/EditorImage.d.ts +3 -0
- package/dist/src/EditorImage.js +7 -0
- package/dist/src/SVGLoader.js +7 -8
- package/dist/src/components/AbstractComponent.d.ts +1 -0
- package/dist/src/components/AbstractComponent.js +4 -0
- package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -0
- package/dist/src/components/SVGGlobalAttributesObject.js +3 -0
- package/dist/src/components/Stroke.js +1 -0
- package/dist/src/components/Text.d.ts +11 -8
- package/dist/src/components/Text.js +63 -20
- package/dist/src/components/UnknownSVGObject.d.ts +1 -0
- package/dist/src/components/UnknownSVGObject.js +3 -0
- package/dist/src/components/builders/FreehandLineBuilder.d.ts +9 -2
- package/dist/src/components/builders/FreehandLineBuilder.js +129 -30
- package/dist/src/components/lib.d.ts +2 -2
- package/dist/src/components/lib.js +2 -2
- package/dist/src/rendering/renderers/CanvasRenderer.js +2 -2
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -0
- package/dist/src/rendering/renderers/SVGRenderer.js +49 -22
- package/dist/src/testing/beforeEachFile.js +4 -0
- package/dist/src/toolbar/HTMLToolbar.js +2 -3
- package/dist/src/toolbar/IconProvider.d.ts +30 -0
- package/dist/src/toolbar/IconProvider.js +417 -0
- package/dist/src/toolbar/lib.d.ts +1 -1
- package/dist/src/toolbar/lib.js +1 -2
- package/dist/src/toolbar/localization.d.ts +0 -1
- package/dist/src/toolbar/localization.js +0 -1
- package/dist/src/toolbar/makeColorInput.js +1 -2
- package/dist/src/toolbar/widgets/BaseWidget.js +1 -2
- package/dist/src/toolbar/widgets/EraserToolWidget.js +1 -2
- package/dist/src/toolbar/widgets/HandToolWidget.d.ts +5 -3
- package/dist/src/toolbar/widgets/HandToolWidget.js +35 -12
- package/dist/src/toolbar/widgets/PenToolWidget.js +10 -8
- package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +3 -0
- package/dist/src/toolbar/widgets/SelectionToolWidget.js +20 -7
- package/dist/src/toolbar/widgets/TextToolWidget.js +1 -2
- package/dist/src/tools/PanZoom.d.ts +1 -1
- package/dist/src/tools/PanZoom.js +4 -1
- package/dist/src/tools/PasteHandler.js +2 -22
- package/dist/src/tools/SelectionTool/SelectionTool.d.ts +3 -0
- package/dist/src/tools/SelectionTool/SelectionTool.js +66 -3
- package/dist/src/tools/TextTool.d.ts +4 -0
- package/dist/src/tools/TextTool.js +73 -15
- package/dist/src/tools/ToolController.js +1 -0
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/tools/localization.js +1 -0
- package/package.json +1 -1
- package/src/Editor.toSVG.test.ts +27 -0
- package/src/Editor.ts +15 -9
- package/src/EditorImage.ts +9 -0
- package/src/SVGLoader.test.ts +57 -0
- package/src/SVGLoader.ts +9 -10
- package/src/components/AbstractComponent.ts +5 -0
- package/src/components/SVGGlobalAttributesObject.ts +4 -0
- package/src/components/Stroke.ts +1 -0
- package/src/components/Text.test.ts +3 -18
- package/src/components/Text.ts +78 -25
- package/src/components/UnknownSVGObject.ts +4 -0
- package/src/components/builders/FreehandLineBuilder.ts +162 -34
- package/src/components/lib.ts +3 -3
- package/src/rendering/renderers/CanvasRenderer.ts +2 -2
- package/src/rendering/renderers/SVGRenderer.ts +50 -24
- package/src/testing/beforeEachFile.ts +6 -1
- package/src/toolbar/HTMLToolbar.ts +2 -3
- package/src/toolbar/IconProvider.ts +480 -0
- package/src/toolbar/lib.ts +1 -1
- package/src/toolbar/localization.ts +0 -2
- package/src/toolbar/makeColorInput.ts +1 -2
- package/src/toolbar/widgets/BaseWidget.ts +1 -2
- package/src/toolbar/widgets/EraserToolWidget.ts +1 -2
- package/src/toolbar/widgets/HandToolWidget.ts +42 -20
- package/src/toolbar/widgets/PenToolWidget.ts +11 -8
- package/src/toolbar/widgets/SelectionToolWidget.ts +24 -8
- package/src/toolbar/widgets/TextToolWidget.ts +1 -2
- package/src/tools/PanZoom.ts +4 -1
- package/src/tools/PasteHandler.ts +2 -24
- package/src/tools/SelectionTool/SelectionTool.css +1 -0
- package/src/tools/SelectionTool/SelectionTool.test.ts +40 -0
- package/src/tools/SelectionTool/SelectionTool.ts +73 -4
- package/src/tools/TextTool.ts +82 -17
- package/src/tools/ToolController.ts +1 -0
- package/src/tools/localization.ts +4 -0
- package/typedoc.json +5 -1
- package/dist/src/toolbar/icons.d.ts +0 -20
- package/dist/src/toolbar/icons.js +0 -385
- package/src/toolbar/icons.ts +0 -443
@@ -1,21 +1,19 @@
|
|
1
1
|
import { EditorEventType } from '../../types';
|
2
|
-
import { makeDeleteSelectionIcon, makeDuplicateSelectionIcon, makeResizeViewportIcon, makeSelectionIcon } from '../icons';
|
3
2
|
import ActionButtonWidget from './ActionButtonWidget';
|
4
3
|
import BaseToolWidget from './BaseToolWidget';
|
5
4
|
export default class SelectionToolWidget extends BaseToolWidget {
|
6
5
|
constructor(editor, tool, localization) {
|
7
6
|
super(editor, tool, localization);
|
8
7
|
this.tool = tool;
|
9
|
-
const resizeButton = new ActionButtonWidget(editor, localization, makeResizeViewportIcon, this.localizationTable.resizeImageToSelection, () => {
|
10
|
-
|
11
|
-
this.editor.dispatch(this.editor.setImportExportRect(selection.region));
|
8
|
+
const resizeButton = new ActionButtonWidget(editor, localization, () => editor.icons.makeResizeViewportIcon(), this.localizationTable.resizeImageToSelection, () => {
|
9
|
+
this.resizeImageToSelection();
|
12
10
|
});
|
13
|
-
const deleteButton = new ActionButtonWidget(editor, localization, makeDeleteSelectionIcon, this.localizationTable.deleteSelection, () => {
|
11
|
+
const deleteButton = new ActionButtonWidget(editor, localization, () => editor.icons.makeDeleteSelectionIcon(), this.localizationTable.deleteSelection, () => {
|
14
12
|
const selection = this.tool.getSelection();
|
15
13
|
this.editor.dispatch(selection.deleteSelectedObjects());
|
16
14
|
this.tool.clearSelection();
|
17
15
|
});
|
18
|
-
const duplicateButton = new ActionButtonWidget(editor, localization, makeDuplicateSelectionIcon, this.localizationTable.duplicateSelection, () => {
|
16
|
+
const duplicateButton = new ActionButtonWidget(editor, localization, () => editor.icons.makeDuplicateSelectionIcon(), this.localizationTable.duplicateSelection, () => {
|
19
17
|
const selection = this.tool.getSelection();
|
20
18
|
this.editor.dispatch(selection.duplicateSelectedObjects());
|
21
19
|
});
|
@@ -40,10 +38,25 @@ export default class SelectionToolWidget extends BaseToolWidget {
|
|
40
38
|
}
|
41
39
|
});
|
42
40
|
}
|
41
|
+
resizeImageToSelection() {
|
42
|
+
const selection = this.tool.getSelection();
|
43
|
+
if (selection) {
|
44
|
+
this.editor.dispatch(this.editor.setImportExportRect(selection.region));
|
45
|
+
}
|
46
|
+
}
|
47
|
+
onKeyPress(event) {
|
48
|
+
// Resize image to selection:
|
49
|
+
// Other keys are handled directly by the selection tool.
|
50
|
+
if (event.ctrlKey && event.key === 'r') {
|
51
|
+
this.resizeImageToSelection();
|
52
|
+
return true;
|
53
|
+
}
|
54
|
+
return false;
|
55
|
+
}
|
43
56
|
getTitle() {
|
44
57
|
return this.localizationTable.select;
|
45
58
|
}
|
46
59
|
createIcon() {
|
47
|
-
return makeSelectionIcon();
|
60
|
+
return this.editor.icons.makeSelectionIcon();
|
48
61
|
}
|
49
62
|
}
|
@@ -1,6 +1,5 @@
|
|
1
1
|
import { EditorEventType } from '../../types';
|
2
2
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
3
|
-
import { makeTextIcon } from '../icons';
|
4
3
|
import makeColorInput from '../makeColorInput';
|
5
4
|
import BaseToolWidget from './BaseToolWidget';
|
6
5
|
export default class TextToolWidget extends BaseToolWidget {
|
@@ -21,7 +20,7 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
21
20
|
}
|
22
21
|
createIcon() {
|
23
22
|
const textStyle = this.tool.getTextStyle();
|
24
|
-
return makeTextIcon(textStyle);
|
23
|
+
return this.editor.icons.makeTextIcon(textStyle);
|
25
24
|
}
|
26
25
|
fillDropdown(dropdown) {
|
27
26
|
const fontRow = document.createElement('div');
|
@@ -35,7 +35,7 @@ export default class PanZoom extends BaseTool {
|
|
35
35
|
onGestureCancel(): void;
|
36
36
|
private updateTransform;
|
37
37
|
onWheel({ delta, screenPos }: WheelEvt): boolean;
|
38
|
-
onKeyPress({ key }: KeyPressEvent): boolean;
|
38
|
+
onKeyPress({ key, ctrlKey, altKey }: KeyPressEvent): boolean;
|
39
39
|
setMode(mode: PanZoomMode): void;
|
40
40
|
getMode(): PanZoomMode;
|
41
41
|
}
|
@@ -134,10 +134,13 @@ export default class PanZoom extends BaseTool {
|
|
134
134
|
this.updateTransform(transformUpdate, true);
|
135
135
|
return true;
|
136
136
|
}
|
137
|
-
onKeyPress({ key }) {
|
137
|
+
onKeyPress({ key, ctrlKey, altKey }) {
|
138
138
|
if (!(this.mode & PanZoomMode.Keyboard)) {
|
139
139
|
return false;
|
140
140
|
}
|
141
|
+
if (ctrlKey || altKey) {
|
142
|
+
return false;
|
143
|
+
}
|
141
144
|
// No need to keep the same the transform for keyboard events.
|
142
145
|
this.transform = Viewport.transformBy(Mat33.identity);
|
143
146
|
let translation = Vec2.zero;
|
@@ -14,7 +14,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
|
|
14
14
|
import { TextComponent } from '../components/lib';
|
15
15
|
import { uniteCommands } from '../commands/lib';
|
16
16
|
import SVGLoader from '../SVGLoader';
|
17
|
-
import { Mat33
|
17
|
+
import { Mat33 } from '../math/lib';
|
18
18
|
import BaseTool from './BaseTool';
|
19
19
|
import EditorImage from '../EditorImage';
|
20
20
|
import SelectionTool from './SelectionTool/SelectionTool';
|
@@ -110,27 +110,7 @@ export default class PasteHandler extends BaseTool {
|
|
110
110
|
const defaultTextStyle = { size: 12, fontFamily: 'sans', renderingStyle: { fill: Color4.red } };
|
111
111
|
const pastedTextStyle = (_b = (_a = textTools[0]) === null || _a === void 0 ? void 0 : _a.getTextStyle()) !== null && _b !== void 0 ? _b : defaultTextStyle;
|
112
112
|
const lines = text.split('\n');
|
113
|
-
|
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
|
-
}
|
113
|
+
yield this.addComponentsFromPaste([TextComponent.fromLines(lines, Mat33.identity, pastedTextStyle)]);
|
134
114
|
});
|
135
115
|
}
|
136
116
|
doImagePaste(dataURL) {
|
@@ -10,6 +10,8 @@ export default class SelectionTool extends BaseTool {
|
|
10
10
|
private prevSelectionBox;
|
11
11
|
private selectionBox;
|
12
12
|
private lastEvtTarget;
|
13
|
+
private expandingSelectionBox;
|
14
|
+
private shiftKeyPressed;
|
13
15
|
constructor(editor: Editor, description: string);
|
14
16
|
private makeSelectionBox;
|
15
17
|
private selectionBoxHandlingEvt;
|
@@ -26,6 +28,7 @@ export default class SelectionTool extends BaseTool {
|
|
26
28
|
onCopy(event: CopyEvent): boolean;
|
27
29
|
setEnabled(enabled: boolean): void;
|
28
30
|
getSelection(): Selection | null;
|
31
|
+
getSelectedObjects(): AbstractComponent[];
|
29
32
|
setSelection(objects: AbstractComponent[]): void;
|
30
33
|
clearSelection(): void;
|
31
34
|
}
|
@@ -16,6 +16,8 @@ export default class SelectionTool extends BaseTool {
|
|
16
16
|
super(editor.notifier, description);
|
17
17
|
this.editor = editor;
|
18
18
|
this.lastEvtTarget = null;
|
19
|
+
this.expandingSelectionBox = false;
|
20
|
+
this.shiftKeyPressed = false;
|
19
21
|
this.selectionBoxHandlingEvt = false;
|
20
22
|
this.handleOverlay = document.createElement('div');
|
21
23
|
editor.createHTMLOverlay(this.handleOverlay);
|
@@ -34,10 +36,13 @@ export default class SelectionTool extends BaseTool {
|
|
34
36
|
});
|
35
37
|
}
|
36
38
|
makeSelectionBox(selectionStartPos) {
|
39
|
+
var _a;
|
37
40
|
this.prevSelectionBox = this.selectionBox;
|
38
41
|
this.selectionBox = new Selection(selectionStartPos, this.editor);
|
39
|
-
|
40
|
-
|
42
|
+
if (!this.expandingSelectionBox) {
|
43
|
+
// Remove any previous selection rects
|
44
|
+
(_a = this.prevSelectionBox) === null || _a === void 0 ? void 0 : _a.cancelSelection();
|
45
|
+
}
|
41
46
|
this.selectionBox.addTo(this.handleOverlay);
|
42
47
|
}
|
43
48
|
onPointerDown(event) {
|
@@ -45,8 +50,11 @@ export default class SelectionTool extends BaseTool {
|
|
45
50
|
if (event.allPointers.length === 1 && event.current.isPrimary) {
|
46
51
|
if (this.lastEvtTarget && ((_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.onDragStart(event.current, this.lastEvtTarget))) {
|
47
52
|
this.selectionBoxHandlingEvt = true;
|
53
|
+
this.expandingSelectionBox = false;
|
48
54
|
}
|
49
55
|
else {
|
56
|
+
// Shift key: Combine the new and old selection boxes at the end of the gesture.
|
57
|
+
this.expandingSelectionBox = this.shiftKeyPressed;
|
50
58
|
this.makeSelectionBox(event.current.canvasPos);
|
51
59
|
}
|
52
60
|
return true;
|
@@ -81,6 +89,7 @@ export default class SelectionTool extends BaseTool {
|
|
81
89
|
this.selectionBox = null;
|
82
90
|
}
|
83
91
|
}
|
92
|
+
// Called after a gestureCancel and a pointerUp
|
84
93
|
onGestureEnd() {
|
85
94
|
this.lastEvtTarget = null;
|
86
95
|
if (!this.selectionBox)
|
@@ -105,7 +114,19 @@ export default class SelectionTool extends BaseTool {
|
|
105
114
|
if (!this.selectionBox)
|
106
115
|
return;
|
107
116
|
this.selectionBox.setToPoint(event.current.canvasPos);
|
108
|
-
|
117
|
+
// Were we expanding the previous selection?
|
118
|
+
if (this.expandingSelectionBox && this.prevSelectionBox) {
|
119
|
+
// If so, finish expanding.
|
120
|
+
this.expandingSelectionBox = false;
|
121
|
+
this.selectionBox.resolveToObjects();
|
122
|
+
this.setSelection([
|
123
|
+
...this.selectionBox.getSelectedObjects(),
|
124
|
+
...this.prevSelectionBox.getSelectedObjects(),
|
125
|
+
]);
|
126
|
+
}
|
127
|
+
else {
|
128
|
+
this.onGestureEnd();
|
129
|
+
}
|
109
130
|
}
|
110
131
|
onGestureCancel() {
|
111
132
|
var _a, _b, _c;
|
@@ -118,8 +139,28 @@ export default class SelectionTool extends BaseTool {
|
|
118
139
|
this.selectionBox = this.prevSelectionBox;
|
119
140
|
(_c = this.selectionBox) === null || _c === void 0 ? void 0 : _c.addTo(this.handleOverlay);
|
120
141
|
}
|
142
|
+
this.expandingSelectionBox = false;
|
121
143
|
}
|
122
144
|
onKeyPress(event) {
|
145
|
+
if (this.selectionBox && event.ctrlKey && event.key === 'd') {
|
146
|
+
// Handle duplication on key up — we don't want to accidentally duplicate
|
147
|
+
// many times.
|
148
|
+
return true;
|
149
|
+
}
|
150
|
+
else if (event.key === 'a' && event.ctrlKey) {
|
151
|
+
// Handle ctrl+A on key up.
|
152
|
+
// Return early to prevent 'a' from moving the selection/view.
|
153
|
+
return true;
|
154
|
+
}
|
155
|
+
else if (event.ctrlKey) {
|
156
|
+
// Don't transform the selection with, for example, ctrl+i.
|
157
|
+
// Pass it to another tool, if apliccable.
|
158
|
+
return false;
|
159
|
+
}
|
160
|
+
else if (event.key === 'Shift') {
|
161
|
+
this.shiftKeyPressed = true;
|
162
|
+
return true;
|
163
|
+
}
|
123
164
|
let rotationSteps = 0;
|
124
165
|
let xTranslateSteps = 0;
|
125
166
|
let yTranslateSteps = 0;
|
@@ -194,6 +235,20 @@ export default class SelectionTool extends BaseTool {
|
|
194
235
|
return handled;
|
195
236
|
}
|
196
237
|
onKeyUp(evt) {
|
238
|
+
if (evt.key === 'Shift') {
|
239
|
+
this.shiftKeyPressed = false;
|
240
|
+
return true;
|
241
|
+
}
|
242
|
+
else if (evt.ctrlKey) {
|
243
|
+
if (this.selectionBox && evt.key === 'd') {
|
244
|
+
this.editor.dispatch(this.selectionBox.duplicateSelectedObjects());
|
245
|
+
return true;
|
246
|
+
}
|
247
|
+
else if (evt.key === 'a') {
|
248
|
+
this.setSelection(this.editor.image.getAllElements());
|
249
|
+
return true;
|
250
|
+
}
|
251
|
+
}
|
197
252
|
if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
|
198
253
|
this.selectionBox.finalizeTransform();
|
199
254
|
return true;
|
@@ -244,10 +299,18 @@ export default class SelectionTool extends BaseTool {
|
|
244
299
|
}
|
245
300
|
}
|
246
301
|
// Get the object responsible for displaying this' selection.
|
302
|
+
// @internal
|
247
303
|
getSelection() {
|
248
304
|
return this.selectionBox;
|
249
305
|
}
|
306
|
+
getSelectedObjects() {
|
307
|
+
var _a, _b;
|
308
|
+
return (_b = (_a = this.selectionBox) === null || _a === void 0 ? void 0 : _a.getSelectedObjects()) !== null && _b !== void 0 ? _b : [];
|
309
|
+
}
|
310
|
+
// Select the given `objects`. Any non-selectable objects in `objects` are ignored.
|
250
311
|
setSelection(objects) {
|
312
|
+
// Only select selectable objects.
|
313
|
+
objects = objects.filter(obj => obj.isSelectable());
|
251
314
|
let bbox = null;
|
252
315
|
for (const object of objects) {
|
253
316
|
if (bbox) {
|
@@ -13,9 +13,12 @@ export default class TextTool extends BaseTool {
|
|
13
13
|
private textTargetPosition;
|
14
14
|
private textMeasuringCtx;
|
15
15
|
private textRotation;
|
16
|
+
private textScale;
|
17
|
+
private removeExistingCommand;
|
16
18
|
constructor(editor: Editor, description: string, localizationTable: ToolLocalization);
|
17
19
|
private getTextAscent;
|
18
20
|
private flushInput;
|
21
|
+
private getTextScaleMatrix;
|
19
22
|
private updateTextInput;
|
20
23
|
private startTextInput;
|
21
24
|
setEnabled(enabled: boolean): void;
|
@@ -26,4 +29,5 @@ export default class TextTool extends BaseTool {
|
|
26
29
|
setColor(color: Color4): void;
|
27
30
|
setFontSize(size: number): void;
|
28
31
|
getTextStyle(): TextStyle;
|
32
|
+
private setTextStyle;
|
29
33
|
}
|
@@ -1,10 +1,14 @@
|
|
1
1
|
import Color4 from '../Color4';
|
2
|
-
import
|
2
|
+
import TextComponent from '../components/Text';
|
3
3
|
import EditorImage from '../EditorImage';
|
4
|
+
import Rect2 from '../math/Rect2';
|
4
5
|
import Mat33 from '../math/Mat33';
|
6
|
+
import { Vec2 } from '../math/Vec2';
|
5
7
|
import { PointerDevice } from '../Pointer';
|
6
8
|
import { EditorEventType } from '../types';
|
7
9
|
import BaseTool from './BaseTool';
|
10
|
+
import Erase from '../commands/Erase';
|
11
|
+
import uniteCommands from '../commands/uniteCommands';
|
8
12
|
const overlayCssClass = 'textEditorOverlay';
|
9
13
|
export default class TextTool extends BaseTool {
|
10
14
|
constructor(editor, description, localizationTable) {
|
@@ -14,6 +18,8 @@ export default class TextTool extends BaseTool {
|
|
14
18
|
this.textInputElem = null;
|
15
19
|
this.textTargetPosition = null;
|
16
20
|
this.textMeasuringCtx = null;
|
21
|
+
this.textScale = Vec2.of(1, 1);
|
22
|
+
this.removeExistingCommand = null;
|
17
23
|
this.textStyle = {
|
18
24
|
size: 32,
|
19
25
|
fontFamily: 'sans-serif',
|
@@ -29,10 +35,18 @@ export default class TextTool extends BaseTool {
|
|
29
35
|
overflow: visible;
|
30
36
|
}
|
31
37
|
|
32
|
-
.${overlayCssClass}
|
38
|
+
.${overlayCssClass} textarea {
|
33
39
|
background-color: rgba(0, 0, 0, 0);
|
40
|
+
|
41
|
+
white-space: pre;
|
42
|
+
|
43
|
+
padding: 0;
|
44
|
+
margin: 0;
|
34
45
|
border: none;
|
35
46
|
padding: 0;
|
47
|
+
|
48
|
+
min-width: 100px;
|
49
|
+
min-height: 1.1em;
|
36
50
|
}
|
37
51
|
`);
|
38
52
|
this.editor.createHTMLOverlay(this.textEditOverlay);
|
@@ -42,7 +56,7 @@ export default class TextTool extends BaseTool {
|
|
42
56
|
var _a;
|
43
57
|
(_a = this.textMeasuringCtx) !== null && _a !== void 0 ? _a : (this.textMeasuringCtx = document.createElement('canvas').getContext('2d'));
|
44
58
|
if (this.textMeasuringCtx) {
|
45
|
-
|
59
|
+
TextComponent.applyTextStyles(this.textMeasuringCtx, style);
|
46
60
|
return this.textMeasuringCtx.measureText(text).actualBoundingBoxAscent;
|
47
61
|
}
|
48
62
|
// Estimate
|
@@ -50,18 +64,29 @@ export default class TextTool extends BaseTool {
|
|
50
64
|
}
|
51
65
|
flushInput() {
|
52
66
|
if (this.textInputElem && this.textTargetPosition) {
|
53
|
-
const content = this.textInputElem.value;
|
67
|
+
const content = this.textInputElem.value.trimEnd();
|
54
68
|
this.textInputElem.remove();
|
55
69
|
this.textInputElem = null;
|
56
70
|
if (content === '') {
|
57
71
|
return;
|
58
72
|
}
|
59
|
-
const textTransform = Mat33.translation(this.textTargetPosition).rightMul(Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas())).rightMul(Mat33.zRotation(this.textRotation));
|
60
|
-
const textComponent =
|
73
|
+
const textTransform = Mat33.translation(this.textTargetPosition).rightMul(this.getTextScaleMatrix()).rightMul(Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas())).rightMul(Mat33.zRotation(this.textRotation));
|
74
|
+
const textComponent = TextComponent.fromLines(content.split('\n'), textTransform, this.textStyle);
|
61
75
|
const action = EditorImage.addElement(textComponent);
|
62
|
-
this.
|
76
|
+
if (this.removeExistingCommand) {
|
77
|
+
// Unapply so that `removeExistingCommand` can be added to the undo stack.
|
78
|
+
this.removeExistingCommand.unapply(this.editor);
|
79
|
+
this.editor.dispatch(uniteCommands([this.removeExistingCommand, action]));
|
80
|
+
this.removeExistingCommand = null;
|
81
|
+
}
|
82
|
+
else {
|
83
|
+
this.editor.dispatch(action);
|
84
|
+
}
|
63
85
|
}
|
64
86
|
}
|
87
|
+
getTextScaleMatrix() {
|
88
|
+
return Mat33.scaling2D(this.textScale.times(1 / this.editor.viewport.getSizeOfPixelOnCanvas()));
|
89
|
+
}
|
65
90
|
updateTextInput() {
|
66
91
|
var _a, _b, _c;
|
67
92
|
if (!this.textInputElem || !this.textTargetPosition) {
|
@@ -70,7 +95,6 @@ export default class TextTool extends BaseTool {
|
|
70
95
|
}
|
71
96
|
const viewport = this.editor.viewport;
|
72
97
|
const textScreenPos = viewport.canvasToScreen(this.textTargetPosition);
|
73
|
-
this.textInputElem.type = 'text';
|
74
98
|
this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;
|
75
99
|
this.textInputElem.style.fontFamily = this.textStyle.fontFamily;
|
76
100
|
this.textInputElem.style.fontVariant = (_b = this.textStyle.fontVariant) !== null && _b !== void 0 ? _b : '';
|
@@ -81,22 +105,27 @@ export default class TextTool extends BaseTool {
|
|
81
105
|
this.textInputElem.style.left = `${textScreenPos.x}px`;
|
82
106
|
this.textInputElem.style.top = `${textScreenPos.y}px`;
|
83
107
|
this.textInputElem.style.margin = '0';
|
108
|
+
this.textInputElem.style.width = `${this.textInputElem.scrollWidth}px`;
|
109
|
+
this.textInputElem.style.height = `${this.textInputElem.scrollHeight}px`;
|
84
110
|
const rotation = this.textRotation + viewport.getRotationAngle();
|
85
111
|
const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
|
86
|
-
|
112
|
+
const scale = this.getTextScaleMatrix();
|
113
|
+
this.textInputElem.style.transform = `${scale.toCSSMatrix()} rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
|
87
114
|
this.textInputElem.style.transformOrigin = 'top left';
|
88
115
|
}
|
89
116
|
startTextInput(textCanvasPos, initialText) {
|
90
117
|
this.flushInput();
|
91
|
-
this.textInputElem = document.createElement('
|
118
|
+
this.textInputElem = document.createElement('textarea');
|
92
119
|
this.textInputElem.value = initialText;
|
120
|
+
this.textInputElem.style.display = 'inline-block';
|
93
121
|
this.textTargetPosition = textCanvasPos;
|
94
122
|
this.textRotation = -this.editor.viewport.getRotationAngle();
|
123
|
+
this.textScale = Vec2.of(1, 1).times(this.editor.viewport.getSizeOfPixelOnCanvas());
|
95
124
|
this.updateTextInput();
|
96
125
|
this.textInputElem.oninput = () => {
|
97
|
-
var _a;
|
98
126
|
if (this.textInputElem) {
|
99
|
-
this.textInputElem.
|
127
|
+
this.textInputElem.style.width = `${this.textInputElem.scrollWidth}px`;
|
128
|
+
this.textInputElem.style.height = `${this.textInputElem.scrollHeight}px`;
|
100
129
|
}
|
101
130
|
};
|
102
131
|
this.textInputElem.onblur = () => {
|
@@ -105,8 +134,8 @@ export default class TextTool extends BaseTool {
|
|
105
134
|
setTimeout(() => this.flushInput(), 0);
|
106
135
|
};
|
107
136
|
this.textInputElem.onkeyup = (evt) => {
|
108
|
-
var _a;
|
109
|
-
if (evt.key === 'Enter') {
|
137
|
+
var _a, _b;
|
138
|
+
if (evt.key === 'Enter' && !evt.shiftKey) {
|
110
139
|
this.flushInput();
|
111
140
|
this.editor.focus();
|
112
141
|
}
|
@@ -115,6 +144,8 @@ export default class TextTool extends BaseTool {
|
|
115
144
|
(_a = this.textInputElem) === null || _a === void 0 ? void 0 : _a.remove();
|
116
145
|
this.textInputElem = null;
|
117
146
|
this.editor.focus();
|
147
|
+
(_b = this.removeExistingCommand) === null || _b === void 0 ? void 0 : _b.unapply(this.editor);
|
148
|
+
this.removeExistingCommand = null;
|
118
149
|
}
|
119
150
|
};
|
120
151
|
this.textEditOverlay.replaceChildren(this.textInputElem);
|
@@ -132,7 +163,29 @@ export default class TextTool extends BaseTool {
|
|
132
163
|
return false;
|
133
164
|
}
|
134
165
|
if (allPointers.length === 1) {
|
135
|
-
|
166
|
+
// Are we clicking on a text node?
|
167
|
+
const canvasPos = current.canvasPos;
|
168
|
+
const halfTestRegionSize = Vec2.of(2.5, 2.5).times(this.editor.viewport.getSizeOfPixelOnCanvas());
|
169
|
+
const testRegion = Rect2.fromCorners(canvasPos.minus(halfTestRegionSize), canvasPos.plus(halfTestRegionSize));
|
170
|
+
const targetNodes = this.editor.image.getElementsIntersectingRegion(testRegion);
|
171
|
+
const targetTextNodes = targetNodes.filter(node => node instanceof TextComponent);
|
172
|
+
if (targetTextNodes.length > 0) {
|
173
|
+
const targetNode = targetTextNodes[targetTextNodes.length - 1];
|
174
|
+
this.setTextStyle(targetNode.getTextStyle());
|
175
|
+
// Create and temporarily apply removeExistingCommand.
|
176
|
+
this.removeExistingCommand = new Erase([targetNode]);
|
177
|
+
this.removeExistingCommand.apply(this.editor);
|
178
|
+
this.startTextInput(targetNode.getBaselinePos(), targetNode.getText());
|
179
|
+
const transform = targetNode.getTransform();
|
180
|
+
this.textRotation = transform.transformVec3(Vec2.unitX).angle();
|
181
|
+
const scaleFactor = transform.transformVec3(Vec2.unitX).magnitude();
|
182
|
+
this.textScale = Vec2.of(1, 1).times(scaleFactor);
|
183
|
+
this.updateTextInput();
|
184
|
+
}
|
185
|
+
else {
|
186
|
+
this.removeExistingCommand = null;
|
187
|
+
this.startTextInput(current.canvasPos, '');
|
188
|
+
}
|
136
189
|
return true;
|
137
190
|
}
|
138
191
|
return false;
|
@@ -169,4 +222,9 @@ export default class TextTool extends BaseTool {
|
|
169
222
|
getTextStyle() {
|
170
223
|
return this.textStyle;
|
171
224
|
}
|
225
|
+
setTextStyle(style) {
|
226
|
+
// Copy the style — we may change parts of it.
|
227
|
+
this.textStyle = Object.assign({}, style);
|
228
|
+
this.dispatchUpdateEvent();
|
229
|
+
}
|
172
230
|
}
|
@@ -29,6 +29,7 @@ export default class ToolController {
|
|
29
29
|
new Eraser(editor, localization.eraserTool),
|
30
30
|
new SelectionTool(editor, localization.selectionTool),
|
31
31
|
new TextTool(editor, localization.textTool, localization),
|
32
|
+
new PanZoom(editor, PanZoomMode.SinglePointerGestures, localization.anyDevicePanning)
|
32
33
|
];
|
33
34
|
this.tools = [
|
34
35
|
new PipetteTool(editor, localization.pipetteTool),
|
@@ -12,6 +12,7 @@ export interface ToolLocalization {
|
|
12
12
|
enterTextToInsert: string;
|
13
13
|
changeTool: string;
|
14
14
|
pasteHandler: string;
|
15
|
+
anyDevicePanning: string;
|
15
16
|
copied: (count: number, description: string) => string;
|
16
17
|
pasted: (count: number, description: string) => string;
|
17
18
|
toolEnabledAnnouncement: (toolName: string) => string;
|
@@ -12,6 +12,7 @@ export const defaultToolLocalization = {
|
|
12
12
|
enterTextToInsert: 'Text to insert',
|
13
13
|
changeTool: 'Change tool',
|
14
14
|
pasteHandler: 'Copy paste handler',
|
15
|
+
anyDevicePanning: 'Any device panning',
|
15
16
|
copied: (count, description) => `Copied ${count} ${description}`,
|
16
17
|
pasted: (count, description) => `Pasted ${count} ${description}`,
|
17
18
|
toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
|
package/package.json
CHANGED
@@ -0,0 +1,27 @@
|
|
1
|
+
import { TextStyle } from './components/Text';
|
2
|
+
import { Color4, Mat33, Rect2, TextComponent, EditorImage, Vec2 } from './lib';
|
3
|
+
import createEditor from './testing/createEditor';
|
4
|
+
|
5
|
+
describe('Editor.toSVG', () => {
|
6
|
+
it('should correctly nest text objects', async () => {
|
7
|
+
const editor = createEditor();
|
8
|
+
const textStyle: TextStyle = {
|
9
|
+
fontFamily: 'sans', size: 12, renderingStyle: { fill: Color4.black }
|
10
|
+
};
|
11
|
+
const text = new TextComponent([
|
12
|
+
'Testing...',
|
13
|
+
new TextComponent([ 'Test 2' ], Mat33.translation(Vec2.of(0, 100)), textStyle),
|
14
|
+
], Mat33.identity, textStyle);
|
15
|
+
editor.dispatch(EditorImage.addElement(text));
|
16
|
+
|
17
|
+
const matches = editor.image.getElementsIntersectingRegion(new Rect2(4, -100, 100, 100));
|
18
|
+
expect(matches).toHaveLength(1);
|
19
|
+
expect(text).not.toBeNull();
|
20
|
+
|
21
|
+
const asSVG = editor.toSVG();
|
22
|
+
const allTSpans = [ ...asSVG.querySelectorAll('tspan') ];
|
23
|
+
expect(allTSpans).toHaveLength(1);
|
24
|
+
expect(allTSpans[0].getAttribute('x')).toBe('0');
|
25
|
+
expect(allTSpans[0].getAttribute('y')).toBe('100');
|
26
|
+
});
|
27
|
+
});
|
package/src/Editor.ts
CHANGED
@@ -37,6 +37,8 @@ import Mat33 from './math/Mat33';
|
|
37
37
|
import Rect2 from './math/Rect2';
|
38
38
|
import { EditorLocalization } from './localization';
|
39
39
|
import getLocalizationTable from './localizations/getLocalizationTable';
|
40
|
+
import IconProvider from './toolbar/IconProvider';
|
41
|
+
import { toRoundedString } from './math/rounding';
|
40
42
|
|
41
43
|
type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';
|
42
44
|
type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent)=>boolean;
|
@@ -58,6 +60,8 @@ export interface EditorSettings {
|
|
58
60
|
/** Minimum zoom fraction (e.g. 0.5 → 50% zoom). */
|
59
61
|
minZoom: number,
|
60
62
|
maxZoom: number,
|
63
|
+
|
64
|
+
iconProvider: IconProvider,
|
61
65
|
}
|
62
66
|
|
63
67
|
// { @inheritDoc Editor! }
|
@@ -101,22 +105,22 @@ export class Editor {
|
|
101
105
|
* editor.dispatch(addElementCommand);
|
102
106
|
* ```
|
103
107
|
*/
|
104
|
-
public image: EditorImage;
|
108
|
+
public readonly image: EditorImage;
|
105
109
|
|
106
110
|
/** Viewport for the exported/imported image. */
|
107
111
|
private importExportViewport: Viewport;
|
108
112
|
|
109
113
|
/** @internal */
|
110
|
-
public localization: EditorLocalization;
|
114
|
+
public readonly localization: EditorLocalization;
|
111
115
|
|
112
|
-
public
|
113
|
-
public
|
116
|
+
public readonly icons: IconProvider;
|
117
|
+
public readonly viewport: Viewport;
|
118
|
+
public readonly toolController: ToolController;
|
114
119
|
|
115
120
|
/**
|
116
121
|
* Global event dispatcher/subscriber.
|
117
|
-
* @see {@link types.EditorEventType}
|
118
122
|
*/
|
119
|
-
public notifier: EditorNotifier;
|
123
|
+
public readonly notifier: EditorNotifier;
|
120
124
|
|
121
125
|
private loadingWarning: HTMLElement;
|
122
126
|
private accessibilityAnnounceArea: HTMLElement;
|
@@ -164,7 +168,9 @@ export class Editor {
|
|
164
168
|
localization: this.localization,
|
165
169
|
minZoom: settings.minZoom ?? 2e-10,
|
166
170
|
maxZoom: settings.maxZoom ?? 1e12,
|
171
|
+
iconProvider: settings.iconProvider ?? new IconProvider(),
|
167
172
|
};
|
173
|
+
this.icons = this.settings.iconProvider;
|
168
174
|
|
169
175
|
this.container = document.createElement('div');
|
170
176
|
this.renderingRegion = document.createElement('div');
|
@@ -834,9 +840,9 @@ export class Editor {
|
|
834
840
|
|
835
841
|
// Just show the main region
|
836
842
|
const rect = importExportViewport.visibleRect;
|
837
|
-
result.setAttribute('viewBox',
|
838
|
-
result.setAttribute('width',
|
839
|
-
result.setAttribute('height',
|
843
|
+
result.setAttribute('viewBox', [rect.x, rect.y, rect.w, rect.h].map(part => toRoundedString(part)).join(' '));
|
844
|
+
result.setAttribute('width', toRoundedString(rect.w));
|
845
|
+
result.setAttribute('height', toRoundedString(rect.h));
|
840
846
|
|
841
847
|
// Ensure the image can be identified as an SVG if downloaded.
|
842
848
|
// See https://jwatt.org/svg/authoring/
|
package/src/EditorImage.ts
CHANGED
@@ -54,6 +54,15 @@ export default class EditorImage {
|
|
54
54
|
}
|
55
55
|
}
|
56
56
|
|
57
|
+
/** @returns all elements in the image, sorted by z-index. This can be slow for large images. */
|
58
|
+
public getAllElements() {
|
59
|
+
const leaves = this.root.getLeaves();
|
60
|
+
sortLeavesByZIndex(leaves);
|
61
|
+
|
62
|
+
return leaves.map(leaf => leaf.getContent()!);
|
63
|
+
}
|
64
|
+
|
65
|
+
/** @returns a list of `AbstractComponent`s intersecting `region`, sorted by z-index. */
|
57
66
|
public getElementsIntersectingRegion(region: Rect2): AbstractComponent[] {
|
58
67
|
const leaves = this.root.getLeavesIntersectingRegion(region);
|
59
68
|
sortLeavesByZIndex(leaves);
|