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
@@ -7,7 +7,6 @@ import Editor from '../../Editor';
|
|
7
7
|
import Pen from '../../tools/Pen';
|
8
8
|
import { EditorEventType, KeyPressEvent } from '../../types';
|
9
9
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
10
|
-
import { makeIconFromFactory, makePenIcon } from '../icons';
|
11
10
|
import { ToolbarLocalization } from '../localization';
|
12
11
|
import makeColorInput from '../makeColorInput';
|
13
12
|
import BaseToolWidget from './BaseToolWidget';
|
@@ -77,10 +76,10 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
77
76
|
// Use a square-root scale to prevent the pen's tip from overflowing.
|
78
77
|
const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
|
79
78
|
const color = this.tool.getColor();
|
80
|
-
return makePenIcon(scale, color.toHexString());
|
79
|
+
return this.editor.icons.makePenIcon(scale, color.toHexString());
|
81
80
|
} else {
|
82
81
|
const strokeFactory = this.tool.getStrokeFactory();
|
83
|
-
return makeIconFromFactory(this.tool, strokeFactory);
|
82
|
+
return this.editor.icons.makeIconFromFactory(this.tool, strokeFactory);
|
84
83
|
}
|
85
84
|
}
|
86
85
|
|
@@ -106,12 +105,16 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
106
105
|
objectSelectLabel.innerText = this.localizationTable.selectObjectType;
|
107
106
|
objectSelectLabel.setAttribute('for', objectTypeSelect.id);
|
108
107
|
|
108
|
+
// Use a logarithmic scale for thicknessInput (finer control over thinner strokewidths.)
|
109
|
+
const inverseThicknessInputFn = (t: number) => Math.log10(t);
|
110
|
+
const thicknessInputFn = (t: number) => 10**t;
|
111
|
+
|
109
112
|
thicknessInput.type = 'range';
|
110
|
-
thicknessInput.min =
|
111
|
-
thicknessInput.max =
|
112
|
-
thicknessInput.step = '1';
|
113
|
+
thicknessInput.min = `${inverseThicknessInputFn(2)}`;
|
114
|
+
thicknessInput.max = `${inverseThicknessInputFn(400)}`;
|
115
|
+
thicknessInput.step = '0.1';
|
113
116
|
thicknessInput.oninput = () => {
|
114
|
-
this.tool.setThickness(parseFloat(thicknessInput.value)
|
117
|
+
this.tool.setThickness(thicknessInputFn(parseFloat(thicknessInput.value)));
|
115
118
|
};
|
116
119
|
thicknessRow.appendChild(thicknessLabel);
|
117
120
|
thicknessRow.appendChild(thicknessInput);
|
@@ -143,7 +146,7 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
143
146
|
|
144
147
|
this.updateInputs = () => {
|
145
148
|
colorInput.value = this.tool.getColor().toHexString();
|
146
|
-
thicknessInput.value =
|
149
|
+
thicknessInput.value = inverseThicknessInputFn(this.tool.getThickness()).toString();
|
147
150
|
|
148
151
|
objectTypeSelect.replaceChildren();
|
149
152
|
for (let i = 0; i < this.penTypes.length; i ++) {
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import Editor from '../../Editor';
|
2
2
|
import SelectionTool from '../../tools/SelectionTool/SelectionTool';
|
3
|
-
import { EditorEventType } from '../../types';
|
4
|
-
import { makeDeleteSelectionIcon, makeDuplicateSelectionIcon, makeResizeViewportIcon, makeSelectionIcon } from '../icons';
|
3
|
+
import { EditorEventType, KeyPressEvent } from '../../types';
|
5
4
|
import { ToolbarLocalization } from '../localization';
|
6
5
|
import ActionButtonWidget from './ActionButtonWidget';
|
7
6
|
import BaseToolWidget from './BaseToolWidget';
|
@@ -14,16 +13,15 @@ export default class SelectionToolWidget extends BaseToolWidget {
|
|
14
13
|
|
15
14
|
const resizeButton = new ActionButtonWidget(
|
16
15
|
editor, localization,
|
17
|
-
makeResizeViewportIcon,
|
16
|
+
() => editor.icons.makeResizeViewportIcon(),
|
18
17
|
this.localizationTable.resizeImageToSelection,
|
19
18
|
() => {
|
20
|
-
|
21
|
-
this.editor.dispatch(this.editor.setImportExportRect(selection!.region));
|
19
|
+
this.resizeImageToSelection();
|
22
20
|
},
|
23
21
|
);
|
24
22
|
const deleteButton = new ActionButtonWidget(
|
25
23
|
editor, localization,
|
26
|
-
makeDeleteSelectionIcon,
|
24
|
+
() => editor.icons.makeDeleteSelectionIcon(),
|
27
25
|
this.localizationTable.deleteSelection,
|
28
26
|
() => {
|
29
27
|
const selection = this.tool.getSelection();
|
@@ -33,7 +31,7 @@ export default class SelectionToolWidget extends BaseToolWidget {
|
|
33
31
|
);
|
34
32
|
const duplicateButton = new ActionButtonWidget(
|
35
33
|
editor, localization,
|
36
|
-
makeDuplicateSelectionIcon,
|
34
|
+
() => editor.icons.makeDuplicateSelectionIcon(),
|
37
35
|
this.localizationTable.duplicateSelection,
|
38
36
|
() => {
|
39
37
|
const selection = this.tool.getSelection();
|
@@ -67,11 +65,29 @@ export default class SelectionToolWidget extends BaseToolWidget {
|
|
67
65
|
});
|
68
66
|
}
|
69
67
|
|
68
|
+
private resizeImageToSelection() {
|
69
|
+
const selection = this.tool.getSelection();
|
70
|
+
if (selection) {
|
71
|
+
this.editor.dispatch(this.editor.setImportExportRect(selection.region));
|
72
|
+
}
|
73
|
+
}
|
74
|
+
|
75
|
+
protected onKeyPress(event: KeyPressEvent): boolean {
|
76
|
+
// Resize image to selection:
|
77
|
+
// Other keys are handled directly by the selection tool.
|
78
|
+
if (event.ctrlKey && event.key === 'r') {
|
79
|
+
this.resizeImageToSelection();
|
80
|
+
return true;
|
81
|
+
}
|
82
|
+
|
83
|
+
return false;
|
84
|
+
}
|
85
|
+
|
70
86
|
protected getTitle(): string {
|
71
87
|
return this.localizationTable.select;
|
72
88
|
}
|
73
89
|
|
74
90
|
protected createIcon(): Element {
|
75
|
-
return makeSelectionIcon();
|
91
|
+
return this.editor.icons.makeSelectionIcon();
|
76
92
|
}
|
77
93
|
}
|
@@ -2,7 +2,6 @@ import Editor from '../../Editor';
|
|
2
2
|
import TextTool from '../../tools/TextTool';
|
3
3
|
import { EditorEventType } from '../../types';
|
4
4
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
5
|
-
import { makeTextIcon } from '../icons';
|
6
5
|
import { ToolbarLocalization } from '../localization';
|
7
6
|
import makeColorInput from '../makeColorInput';
|
8
7
|
import BaseToolWidget from './BaseToolWidget';
|
@@ -26,7 +25,7 @@ export default class TextToolWidget extends BaseToolWidget {
|
|
26
25
|
|
27
26
|
protected createIcon(): Element {
|
28
27
|
const textStyle = this.tool.getTextStyle();
|
29
|
-
return makeTextIcon(textStyle);
|
28
|
+
return this.editor.icons.makeTextIcon(textStyle);
|
30
29
|
}
|
31
30
|
|
32
31
|
private static idCounter: number = 0;
|
package/src/tools/PanZoom.ts
CHANGED
@@ -182,10 +182,13 @@ export default class PanZoom extends BaseTool {
|
|
182
182
|
return true;
|
183
183
|
}
|
184
184
|
|
185
|
-
public onKeyPress({ key }: KeyPressEvent): boolean {
|
185
|
+
public onKeyPress({ key, ctrlKey, altKey }: KeyPressEvent): boolean {
|
186
186
|
if (!(this.mode & PanZoomMode.Keyboard)) {
|
187
187
|
return false;
|
188
188
|
}
|
189
|
+
if (ctrlKey || altKey) {
|
190
|
+
return false;
|
191
|
+
}
|
189
192
|
|
190
193
|
// No need to keep the same the transform for keyboard events.
|
191
194
|
this.transform = Viewport.transformBy(Mat33.identity);
|
@@ -8,7 +8,7 @@ import { AbstractComponent, TextComponent } from '../components/lib';
|
|
8
8
|
import { Command, uniteCommands } from '../commands/lib';
|
9
9
|
import SVGLoader from '../SVGLoader';
|
10
10
|
import { PasteEvent } from '../types';
|
11
|
-
import { Mat33, Rect2
|
11
|
+
import { Mat33, Rect2 } from '../math/lib';
|
12
12
|
import BaseTool from './BaseTool';
|
13
13
|
import EditorImage from '../EditorImage';
|
14
14
|
import SelectionTool from './SelectionTool/SelectionTool';
|
@@ -125,29 +125,7 @@ export default class PasteHandler extends BaseTool {
|
|
125
125
|
const pastedTextStyle: TextStyle = textTools[0]?.getTextStyle() ?? defaultTextStyle;
|
126
126
|
|
127
127
|
const lines = text.split('\n');
|
128
|
-
|
129
|
-
const components: TextComponent[] = [];
|
130
|
-
|
131
|
-
for (const line of lines) {
|
132
|
-
let position = Vec2.zero;
|
133
|
-
if (lastComponent) {
|
134
|
-
const lineMargin = Math.floor(pastedTextStyle.size);
|
135
|
-
position = lastComponent.getBBox().bottomLeft.plus(Vec2.unitY.times(lineMargin));
|
136
|
-
}
|
137
|
-
|
138
|
-
const component = new TextComponent([ line ], Mat33.translation(position), pastedTextStyle);
|
139
|
-
components.push(component);
|
140
|
-
lastComponent = component;
|
141
|
-
}
|
142
|
-
|
143
|
-
if (components.length === 1) {
|
144
|
-
await this.addComponentsFromPaste([ components[0] ]);
|
145
|
-
} else {
|
146
|
-
// Wrap the existing `TextComponent`s --- dragging one component should drag all.
|
147
|
-
await this.addComponentsFromPaste([
|
148
|
-
new TextComponent(components, Mat33.identity, pastedTextStyle)
|
149
|
-
]);
|
150
|
-
}
|
128
|
+
await this.addComponentsFromPaste([ TextComponent.fromLines(lines, Mat33.identity, pastedTextStyle) ]);
|
151
129
|
}
|
152
130
|
|
153
131
|
private async doImagePaste(dataURL: string) {
|
@@ -100,4 +100,44 @@ describe('SelectionTool', () => {
|
|
100
100
|
editor.sendKeyboardEvent(InputEvtType.KeyUpEvent, 'a');
|
101
101
|
expect(editor.viewport.visibleRect.containsPoint(selection.region.center)).toBe(true);
|
102
102
|
});
|
103
|
+
|
104
|
+
it('shift+click should expand an existing selection', () => {
|
105
|
+
const { addTestStrokeCommand: stroke1Command } = createSquareStroke(50);
|
106
|
+
const { addTestStrokeCommand: stroke2Command } = createSquareStroke(500);
|
107
|
+
|
108
|
+
const editor = createEditor();
|
109
|
+
editor.dispatch(stroke1Command);
|
110
|
+
editor.dispatch(stroke2Command);
|
111
|
+
|
112
|
+
// Select the first stroke
|
113
|
+
const selectionTool = getSelectionTool(editor);
|
114
|
+
selectionTool.setEnabled(true);
|
115
|
+
|
116
|
+
// Select the smaller rectangle
|
117
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(40, 40));
|
118
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(100, 100));
|
119
|
+
|
120
|
+
expect(selectionTool.getSelectedObjects()).toHaveLength(1);
|
121
|
+
|
122
|
+
// Shift key down.
|
123
|
+
editor.sendKeyboardEvent(InputEvtType.KeyPressEvent, 'Shift');
|
124
|
+
|
125
|
+
// Select the larger stroke.
|
126
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(200, 200));
|
127
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(600, 600));
|
128
|
+
|
129
|
+
expect(selectionTool.getSelectedObjects()).toHaveLength(2);
|
130
|
+
|
131
|
+
editor.sendKeyboardEvent(InputEvtType.KeyUpEvent, 'Shift');
|
132
|
+
|
133
|
+
// Select the larger stroke without shift pressed
|
134
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(200, 200));
|
135
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(600, 600));
|
136
|
+
expect(selectionTool.getSelectedObjects()).toHaveLength(1);
|
137
|
+
|
138
|
+
// Select nothing
|
139
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(200, 200));
|
140
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(201, 201));
|
141
|
+
expect(selectionTool.getSelectedObjects()).toHaveLength(0);
|
142
|
+
});
|
103
143
|
});
|
@@ -23,6 +23,9 @@ export default class SelectionTool extends BaseTool {
|
|
23
23
|
private selectionBox: Selection|null;
|
24
24
|
private lastEvtTarget: EventTarget|null = null;
|
25
25
|
|
26
|
+
private expandingSelectionBox: boolean = false;
|
27
|
+
private shiftKeyPressed: boolean = false;
|
28
|
+
|
26
29
|
public constructor(private editor: Editor, description: string) {
|
27
30
|
super(editor.notifier, description);
|
28
31
|
|
@@ -50,8 +53,11 @@ export default class SelectionTool extends BaseTool {
|
|
50
53
|
this.selectionBox = new Selection(
|
51
54
|
selectionStartPos, this.editor
|
52
55
|
);
|
53
|
-
|
54
|
-
this.
|
56
|
+
|
57
|
+
if (!this.expandingSelectionBox) {
|
58
|
+
// Remove any previous selection rects
|
59
|
+
this.prevSelectionBox?.cancelSelection();
|
60
|
+
}
|
55
61
|
this.selectionBox.addTo(this.handleOverlay);
|
56
62
|
}
|
57
63
|
|
@@ -60,7 +66,11 @@ export default class SelectionTool extends BaseTool {
|
|
60
66
|
if (event.allPointers.length === 1 && event.current.isPrimary) {
|
61
67
|
if (this.lastEvtTarget && this.selectionBox?.onDragStart(event.current, this.lastEvtTarget)) {
|
62
68
|
this.selectionBoxHandlingEvt = true;
|
63
|
-
|
69
|
+
this.expandingSelectionBox = false;
|
70
|
+
}
|
71
|
+
else {
|
72
|
+
// Shift key: Combine the new and old selection boxes at the end of the gesture.
|
73
|
+
this.expandingSelectionBox = this.shiftKeyPressed;
|
64
74
|
this.makeSelectionBox(event.current.canvasPos);
|
65
75
|
}
|
66
76
|
|
@@ -99,6 +109,7 @@ export default class SelectionTool extends BaseTool {
|
|
99
109
|
}
|
100
110
|
}
|
101
111
|
|
112
|
+
// Called after a gestureCancel and a pointerUp
|
102
113
|
private onGestureEnd() {
|
103
114
|
this.lastEvtTarget = null;
|
104
115
|
|
@@ -127,7 +138,19 @@ export default class SelectionTool extends BaseTool {
|
|
127
138
|
if (!this.selectionBox) return;
|
128
139
|
|
129
140
|
this.selectionBox.setToPoint(event.current.canvasPos);
|
130
|
-
|
141
|
+
|
142
|
+
// Were we expanding the previous selection?
|
143
|
+
if (this.expandingSelectionBox && this.prevSelectionBox) {
|
144
|
+
// If so, finish expanding.
|
145
|
+
this.expandingSelectionBox = false;
|
146
|
+
this.selectionBox.resolveToObjects();
|
147
|
+
this.setSelection([
|
148
|
+
...this.selectionBox.getSelectedObjects(),
|
149
|
+
...this.prevSelectionBox.getSelectedObjects(),
|
150
|
+
]);
|
151
|
+
} else {
|
152
|
+
this.onGestureEnd();
|
153
|
+
}
|
131
154
|
}
|
132
155
|
|
133
156
|
public onGestureCancel(): void {
|
@@ -139,6 +162,8 @@ export default class SelectionTool extends BaseTool {
|
|
139
162
|
this.selectionBox = this.prevSelectionBox;
|
140
163
|
this.selectionBox?.addTo(this.handleOverlay);
|
141
164
|
}
|
165
|
+
|
166
|
+
this.expandingSelectionBox = false;
|
142
167
|
}
|
143
168
|
|
144
169
|
private static handleableKeys = [
|
@@ -150,6 +175,26 @@ export default class SelectionTool extends BaseTool {
|
|
150
175
|
'i', 'I', 'o', 'O',
|
151
176
|
];
|
152
177
|
public onKeyPress(event: KeyPressEvent): boolean {
|
178
|
+
if (this.selectionBox && event.ctrlKey && event.key === 'd') {
|
179
|
+
// Handle duplication on key up — we don't want to accidentally duplicate
|
180
|
+
// many times.
|
181
|
+
return true;
|
182
|
+
}
|
183
|
+
else if (event.key === 'a' && event.ctrlKey) {
|
184
|
+
// Handle ctrl+A on key up.
|
185
|
+
// Return early to prevent 'a' from moving the selection/view.
|
186
|
+
return true;
|
187
|
+
}
|
188
|
+
else if (event.ctrlKey) {
|
189
|
+
// Don't transform the selection with, for example, ctrl+i.
|
190
|
+
// Pass it to another tool, if apliccable.
|
191
|
+
return false;
|
192
|
+
}
|
193
|
+
else if (event.key === 'Shift') {
|
194
|
+
this.shiftKeyPressed = true;
|
195
|
+
return true;
|
196
|
+
}
|
197
|
+
|
153
198
|
let rotationSteps = 0;
|
154
199
|
let xTranslateSteps = 0;
|
155
200
|
let yTranslateSteps = 0;
|
@@ -245,6 +290,21 @@ export default class SelectionTool extends BaseTool {
|
|
245
290
|
}
|
246
291
|
|
247
292
|
public onKeyUp(evt: KeyUpEvent) {
|
293
|
+
if (evt.key === 'Shift') {
|
294
|
+
this.shiftKeyPressed = false;
|
295
|
+
return true;
|
296
|
+
}
|
297
|
+
else if (evt.ctrlKey) {
|
298
|
+
if (this.selectionBox && evt.key === 'd') {
|
299
|
+
this.editor.dispatch(this.selectionBox.duplicateSelectedObjects());
|
300
|
+
return true;
|
301
|
+
}
|
302
|
+
else if (evt.key === 'a') {
|
303
|
+
this.setSelection(this.editor.image.getAllElements());
|
304
|
+
return true;
|
305
|
+
}
|
306
|
+
}
|
307
|
+
|
248
308
|
if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
|
249
309
|
this.selectionBox.finalizeTransform();
|
250
310
|
return true;
|
@@ -307,11 +367,20 @@ export default class SelectionTool extends BaseTool {
|
|
307
367
|
}
|
308
368
|
|
309
369
|
// Get the object responsible for displaying this' selection.
|
370
|
+
// @internal
|
310
371
|
public getSelection(): Selection|null {
|
311
372
|
return this.selectionBox;
|
312
373
|
}
|
313
374
|
|
375
|
+
public getSelectedObjects(): AbstractComponent[] {
|
376
|
+
return this.selectionBox?.getSelectedObjects() ?? [];
|
377
|
+
}
|
378
|
+
|
379
|
+
// Select the given `objects`. Any non-selectable objects in `objects` are ignored.
|
314
380
|
public setSelection(objects: AbstractComponent[]) {
|
381
|
+
// Only select selectable objects.
|
382
|
+
objects = objects.filter(obj => obj.isSelectable());
|
383
|
+
|
315
384
|
let bbox: Rect2|null = null;
|
316
385
|
for (const object of objects) {
|
317
386
|
if (bbox) {
|
package/src/tools/TextTool.ts
CHANGED
@@ -1,23 +1,29 @@
|
|
1
1
|
import Color4 from '../Color4';
|
2
|
-
import
|
2
|
+
import TextComponent, { TextStyle } from '../components/Text';
|
3
3
|
import Editor from '../Editor';
|
4
4
|
import EditorImage from '../EditorImage';
|
5
|
+
import Rect2 from '../math/Rect2';
|
5
6
|
import Mat33 from '../math/Mat33';
|
6
7
|
import { Vec2 } from '../math/Vec2';
|
7
8
|
import { PointerDevice } from '../Pointer';
|
8
9
|
import { EditorEventType, PointerEvt } from '../types';
|
9
10
|
import BaseTool from './BaseTool';
|
10
11
|
import { ToolLocalization } from './localization';
|
12
|
+
import Erase from '../commands/Erase';
|
13
|
+
import uniteCommands from '../commands/uniteCommands';
|
11
14
|
|
12
15
|
const overlayCssClass = 'textEditorOverlay';
|
13
16
|
export default class TextTool extends BaseTool {
|
14
17
|
private textStyle: TextStyle;
|
15
18
|
|
16
19
|
private textEditOverlay: HTMLElement;
|
17
|
-
private textInputElem:
|
20
|
+
private textInputElem: HTMLTextAreaElement|null = null;
|
18
21
|
private textTargetPosition: Vec2|null = null;
|
19
22
|
private textMeasuringCtx: CanvasRenderingContext2D|null = null;
|
20
23
|
private textRotation: number;
|
24
|
+
private textScale: Vec2 = Vec2.of(1, 1);
|
25
|
+
|
26
|
+
private removeExistingCommand: Erase|null = null;
|
21
27
|
|
22
28
|
public constructor(private editor: Editor, description: string, private localizationTable: ToolLocalization) {
|
23
29
|
super(editor.notifier, description);
|
@@ -37,10 +43,18 @@ export default class TextTool extends BaseTool {
|
|
37
43
|
overflow: visible;
|
38
44
|
}
|
39
45
|
|
40
|
-
.${overlayCssClass}
|
46
|
+
.${overlayCssClass} textarea {
|
41
47
|
background-color: rgba(0, 0, 0, 0);
|
48
|
+
|
49
|
+
white-space: pre;
|
50
|
+
|
51
|
+
padding: 0;
|
52
|
+
margin: 0;
|
42
53
|
border: none;
|
43
54
|
padding: 0;
|
55
|
+
|
56
|
+
min-width: 100px;
|
57
|
+
min-height: 1.1em;
|
44
58
|
}
|
45
59
|
`);
|
46
60
|
this.editor.createHTMLOverlay(this.textEditOverlay);
|
@@ -50,7 +64,7 @@ export default class TextTool extends BaseTool {
|
|
50
64
|
private getTextAscent(text: string, style: TextStyle): number {
|
51
65
|
this.textMeasuringCtx ??= document.createElement('canvas').getContext('2d');
|
52
66
|
if (this.textMeasuringCtx) {
|
53
|
-
|
67
|
+
TextComponent.applyTextStyles(this.textMeasuringCtx, style);
|
54
68
|
return this.textMeasuringCtx.measureText(text).actualBoundingBoxAscent;
|
55
69
|
}
|
56
70
|
|
@@ -60,7 +74,7 @@ export default class TextTool extends BaseTool {
|
|
60
74
|
|
61
75
|
private flushInput() {
|
62
76
|
if (this.textInputElem && this.textTargetPosition) {
|
63
|
-
const content = this.textInputElem.value;
|
77
|
+
const content = this.textInputElem.value.trimEnd();
|
64
78
|
this.textInputElem.remove();
|
65
79
|
this.textInputElem = null;
|
66
80
|
|
@@ -70,23 +84,33 @@ export default class TextTool extends BaseTool {
|
|
70
84
|
|
71
85
|
const textTransform = Mat33.translation(
|
72
86
|
this.textTargetPosition
|
87
|
+
).rightMul(
|
88
|
+
this.getTextScaleMatrix()
|
73
89
|
).rightMul(
|
74
90
|
Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas())
|
75
91
|
).rightMul(
|
76
92
|
Mat33.zRotation(this.textRotation)
|
77
93
|
);
|
78
94
|
|
79
|
-
const textComponent =
|
80
|
-
[ content ],
|
81
|
-
textTransform,
|
82
|
-
this.textStyle,
|
83
|
-
);
|
95
|
+
const textComponent = TextComponent.fromLines(content.split('\n'), textTransform, this.textStyle);
|
84
96
|
|
85
97
|
const action = EditorImage.addElement(textComponent);
|
86
|
-
this.
|
98
|
+
if (this.removeExistingCommand) {
|
99
|
+
// Unapply so that `removeExistingCommand` can be added to the undo stack.
|
100
|
+
this.removeExistingCommand.unapply(this.editor);
|
101
|
+
|
102
|
+
this.editor.dispatch(uniteCommands([ this.removeExistingCommand, action ]));
|
103
|
+
this.removeExistingCommand = null;
|
104
|
+
} else {
|
105
|
+
this.editor.dispatch(action);
|
106
|
+
}
|
87
107
|
}
|
88
108
|
}
|
89
109
|
|
110
|
+
private getTextScaleMatrix() {
|
111
|
+
return Mat33.scaling2D(this.textScale.times(1/this.editor.viewport.getSizeOfPixelOnCanvas()));
|
112
|
+
}
|
113
|
+
|
90
114
|
private updateTextInput() {
|
91
115
|
if (!this.textInputElem || !this.textTargetPosition) {
|
92
116
|
this.textInputElem?.remove();
|
@@ -95,7 +119,6 @@ export default class TextTool extends BaseTool {
|
|
95
119
|
|
96
120
|
const viewport = this.editor.viewport;
|
97
121
|
const textScreenPos = viewport.canvasToScreen(this.textTargetPosition);
|
98
|
-
this.textInputElem.type = 'text';
|
99
122
|
this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;
|
100
123
|
this.textInputElem.style.fontFamily = this.textStyle.fontFamily;
|
101
124
|
this.textInputElem.style.fontVariant = this.textStyle.fontVariant ?? '';
|
@@ -108,24 +131,31 @@ export default class TextTool extends BaseTool {
|
|
108
131
|
this.textInputElem.style.top = `${textScreenPos.y}px`;
|
109
132
|
this.textInputElem.style.margin = '0';
|
110
133
|
|
134
|
+
this.textInputElem.style.width = `${this.textInputElem.scrollWidth}px`;
|
135
|
+
this.textInputElem.style.height = `${this.textInputElem.scrollHeight}px`;
|
136
|
+
|
111
137
|
const rotation = this.textRotation + viewport.getRotationAngle();
|
112
138
|
const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
|
113
|
-
|
139
|
+
const scale: Mat33 = this.getTextScaleMatrix();
|
140
|
+
this.textInputElem.style.transform = `${scale.toCSSMatrix()} rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
|
114
141
|
this.textInputElem.style.transformOrigin = 'top left';
|
115
142
|
}
|
116
143
|
|
117
144
|
private startTextInput(textCanvasPos: Vec2, initialText: string) {
|
118
145
|
this.flushInput();
|
119
146
|
|
120
|
-
this.textInputElem = document.createElement('
|
147
|
+
this.textInputElem = document.createElement('textarea');
|
121
148
|
this.textInputElem.value = initialText;
|
149
|
+
this.textInputElem.style.display = 'inline-block';
|
122
150
|
this.textTargetPosition = textCanvasPos;
|
123
151
|
this.textRotation = -this.editor.viewport.getRotationAngle();
|
152
|
+
this.textScale = Vec2.of(1, 1).times(this.editor.viewport.getSizeOfPixelOnCanvas());
|
124
153
|
this.updateTextInput();
|
125
154
|
|
126
155
|
this.textInputElem.oninput = () => {
|
127
156
|
if (this.textInputElem) {
|
128
|
-
this.textInputElem.
|
157
|
+
this.textInputElem.style.width = `${this.textInputElem.scrollWidth}px`;
|
158
|
+
this.textInputElem.style.height = `${this.textInputElem.scrollHeight}px`;
|
129
159
|
}
|
130
160
|
};
|
131
161
|
this.textInputElem.onblur = () => {
|
@@ -134,7 +164,7 @@ export default class TextTool extends BaseTool {
|
|
134
164
|
setTimeout(() => this.flushInput(), 0);
|
135
165
|
};
|
136
166
|
this.textInputElem.onkeyup = (evt) => {
|
137
|
-
if (evt.key === 'Enter') {
|
167
|
+
if (evt.key === 'Enter' && !evt.shiftKey) {
|
138
168
|
this.flushInput();
|
139
169
|
this.editor.focus();
|
140
170
|
} else if (evt.key === 'Escape') {
|
@@ -142,6 +172,9 @@ export default class TextTool extends BaseTool {
|
|
142
172
|
this.textInputElem?.remove();
|
143
173
|
this.textInputElem = null;
|
144
174
|
this.editor.focus();
|
175
|
+
|
176
|
+
this.removeExistingCommand?.unapply(this.editor);
|
177
|
+
this.removeExistingCommand = null;
|
145
178
|
}
|
146
179
|
};
|
147
180
|
|
@@ -165,7 +198,33 @@ export default class TextTool extends BaseTool {
|
|
165
198
|
}
|
166
199
|
|
167
200
|
if (allPointers.length === 1) {
|
168
|
-
|
201
|
+
|
202
|
+
// Are we clicking on a text node?
|
203
|
+
const canvasPos = current.canvasPos;
|
204
|
+
const halfTestRegionSize = Vec2.of(2.5, 2.5).times(this.editor.viewport.getSizeOfPixelOnCanvas());
|
205
|
+
const testRegion = Rect2.fromCorners(canvasPos.minus(halfTestRegionSize), canvasPos.plus(halfTestRegionSize));
|
206
|
+
const targetNodes = this.editor.image.getElementsIntersectingRegion(testRegion);
|
207
|
+
const targetTextNodes = targetNodes.filter(node => node instanceof TextComponent) as TextComponent[];
|
208
|
+
|
209
|
+
if (targetTextNodes.length > 0) {
|
210
|
+
const targetNode = targetTextNodes[targetTextNodes.length - 1];
|
211
|
+
this.setTextStyle(targetNode.getTextStyle());
|
212
|
+
|
213
|
+
// Create and temporarily apply removeExistingCommand.
|
214
|
+
this.removeExistingCommand = new Erase([ targetNode ]);
|
215
|
+
this.removeExistingCommand.apply(this.editor);
|
216
|
+
|
217
|
+
this.startTextInput(targetNode.getBaselinePos(), targetNode.getText());
|
218
|
+
|
219
|
+
const transform = targetNode.getTransform();
|
220
|
+
this.textRotation = transform.transformVec3(Vec2.unitX).angle();
|
221
|
+
const scaleFactor = transform.transformVec3(Vec2.unitX).magnitude();
|
222
|
+
this.textScale = Vec2.of(1, 1).times(scaleFactor);
|
223
|
+
this.updateTextInput();
|
224
|
+
} else {
|
225
|
+
this.removeExistingCommand = null;
|
226
|
+
this.startTextInput(current.canvasPos, '');
|
227
|
+
}
|
169
228
|
return true;
|
170
229
|
}
|
171
230
|
|
@@ -224,4 +283,10 @@ export default class TextTool extends BaseTool {
|
|
224
283
|
public getTextStyle(): TextStyle {
|
225
284
|
return this.textStyle;
|
226
285
|
}
|
286
|
+
|
287
|
+
private setTextStyle(style: TextStyle) {
|
288
|
+
// Copy the style — we may change parts of it.
|
289
|
+
this.textStyle = {...style};
|
290
|
+
this.dispatchUpdateEvent();
|
291
|
+
}
|
227
292
|
}
|
@@ -39,6 +39,7 @@ export default class ToolController {
|
|
39
39
|
new Eraser(editor, localization.eraserTool),
|
40
40
|
new SelectionTool(editor, localization.selectionTool),
|
41
41
|
new TextTool(editor, localization.textTool, localization),
|
42
|
+
new PanZoom(editor, PanZoomMode.SinglePointerGestures, localization.anyDevicePanning)
|
42
43
|
];
|
43
44
|
this.tools = [
|
44
45
|
new PipetteTool(editor, localization.pipetteTool),
|
@@ -15,6 +15,8 @@ export interface ToolLocalization {
|
|
15
15
|
changeTool: string;
|
16
16
|
pasteHandler: string;
|
17
17
|
|
18
|
+
anyDevicePanning: string;
|
19
|
+
|
18
20
|
copied: (count: number, description: string) => string;
|
19
21
|
pasted: (count: number, description: string) => string;
|
20
22
|
|
@@ -38,6 +40,8 @@ export const defaultToolLocalization: ToolLocalization = {
|
|
38
40
|
changeTool: 'Change tool',
|
39
41
|
pasteHandler: 'Copy paste handler',
|
40
42
|
|
43
|
+
anyDevicePanning: 'Any device panning',
|
44
|
+
|
41
45
|
copied: (count: number, description: string) => `Copied ${count} ${description}`,
|
42
46
|
pasted: (count: number, description: string) => `Pasted ${count} ${description}`,
|
43
47
|
|
package/typedoc.json
CHANGED
@@ -1,20 +0,0 @@
|
|
1
|
-
import Color4 from '../Color4';
|
2
|
-
import { ComponentBuilderFactory } from '../components/builders/types';
|
3
|
-
import { TextStyle } from '../components/Text';
|
4
|
-
import Pen from '../tools/Pen';
|
5
|
-
export declare const makeUndoIcon: () => SVGSVGElement;
|
6
|
-
export declare const makeRedoIcon: (mirror?: boolean) => SVGSVGElement;
|
7
|
-
export declare const makeDropdownIcon: () => SVGSVGElement;
|
8
|
-
export declare const makeEraserIcon: () => SVGSVGElement;
|
9
|
-
export declare const makeSelectionIcon: () => SVGSVGElement;
|
10
|
-
export declare const makeHandToolIcon: () => SVGSVGElement;
|
11
|
-
export declare const makeTouchPanningIcon: () => SVGSVGElement;
|
12
|
-
export declare const makeAllDevicePanningIcon: () => SVGSVGElement;
|
13
|
-
export declare const makeZoomIcon: () => SVGSVGElement;
|
14
|
-
export declare const makeTextIcon: (textStyle: TextStyle) => SVGSVGElement;
|
15
|
-
export declare const makePenIcon: (tipThickness: number, color: string | Color4) => SVGSVGElement;
|
16
|
-
export declare const makeIconFromFactory: (pen: Pen, factory: ComponentBuilderFactory) => SVGSVGElement;
|
17
|
-
export declare const makePipetteIcon: (color?: Color4) => SVGSVGElement;
|
18
|
-
export declare const makeResizeViewportIcon: () => SVGSVGElement;
|
19
|
-
export declare const makeDuplicateSelectionIcon: () => SVGSVGElement;
|
20
|
-
export declare const makeDeleteSelectionIcon: () => SVGSVGElement;
|