js-draw 0.6.0 → 0.7.1
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 +11 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +0 -1
- package/dist/src/Editor.js +4 -3
- package/dist/src/SVGLoader.js +2 -2
- package/dist/src/components/Stroke.js +1 -0
- package/dist/src/components/Text.d.ts +10 -5
- package/dist/src/components/Text.js +49 -15
- package/dist/src/components/builders/FreehandLineBuilder.d.ts +9 -2
- package/dist/src/components/builders/FreehandLineBuilder.js +127 -28
- 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/toolbar/IconProvider.d.ts +24 -18
- package/dist/src/toolbar/IconProvider.js +23 -21
- package/dist/src/toolbar/widgets/PenToolWidget.js +8 -5
- package/dist/src/tools/PasteHandler.js +2 -22
- package/dist/src/tools/TextTool.d.ts +4 -0
- package/dist/src/tools/TextTool.js +76 -15
- package/package.json +1 -1
- package/src/Editor.toSVG.test.ts +27 -0
- package/src/Editor.ts +4 -4
- package/src/SVGLoader.test.ts +20 -0
- package/src/SVGLoader.ts +4 -4
- package/src/components/Stroke.ts +1 -0
- package/src/components/Text.test.ts +3 -3
- package/src/components/Text.ts +62 -19
- package/src/components/builders/FreehandLineBuilder.ts +160 -32
- 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/toolbar/IconProvider.ts +24 -20
- package/src/toolbar/widgets/PenToolWidget.ts +9 -5
- package/src/tools/PasteHandler.ts +2 -24
- package/src/tools/TextTool.ts +86 -17
@@ -8,7 +8,7 @@ import Pen from '../tools/Pen';
|
|
8
8
|
import { StrokeDataPoint } from '../types';
|
9
9
|
import Viewport from '../Viewport';
|
10
10
|
|
11
|
-
|
11
|
+
type IconType = SVGSVGElement|HTMLImageElement;
|
12
12
|
|
13
13
|
const svgNamespace = 'http://www.w3.org/2000/svg';
|
14
14
|
const iconColorFill = `
|
@@ -36,13 +36,13 @@ const checkerboardPatternRef = 'url(#checkerboard)';
|
|
36
36
|
// Extend this class and override methods to customize icons.
|
37
37
|
export default class IconProvider {
|
38
38
|
|
39
|
-
public makeUndoIcon() {
|
39
|
+
public makeUndoIcon(): IconType {
|
40
40
|
return this.makeRedoIcon(true);
|
41
41
|
}
|
42
42
|
|
43
43
|
// @param mirror - reflect across the x-axis @internal
|
44
44
|
// @returns a redo icon.
|
45
|
-
public makeRedoIcon(mirror: boolean = false) {
|
45
|
+
public makeRedoIcon(mirror: boolean = false): IconType {
|
46
46
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
47
47
|
icon.innerHTML = `
|
48
48
|
<style>
|
@@ -65,7 +65,7 @@ export default class IconProvider {
|
|
65
65
|
return icon;
|
66
66
|
}
|
67
67
|
|
68
|
-
public makeDropdownIcon() {
|
68
|
+
public makeDropdownIcon(): IconType {
|
69
69
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
70
70
|
icon.innerHTML = `
|
71
71
|
<g>
|
@@ -79,7 +79,7 @@ export default class IconProvider {
|
|
79
79
|
return icon;
|
80
80
|
}
|
81
81
|
|
82
|
-
public makeEraserIcon() {
|
82
|
+
public makeEraserIcon(): IconType {
|
83
83
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
84
84
|
|
85
85
|
// Draw an eraser-like shape
|
@@ -96,7 +96,7 @@ export default class IconProvider {
|
|
96
96
|
return icon;
|
97
97
|
}
|
98
98
|
|
99
|
-
public makeSelectionIcon() {
|
99
|
+
public makeSelectionIcon(): IconType {
|
100
100
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
101
101
|
|
102
102
|
// Draw a cursor-like shape
|
@@ -111,12 +111,16 @@ export default class IconProvider {
|
|
111
111
|
return icon;
|
112
112
|
}
|
113
113
|
|
114
|
+
/**
|
115
|
+
* @param pathData - SVG path data (e.g. `m10,10l30,30z`)
|
116
|
+
* @param fill - A valid CSS color (e.g. `var(--icon-color)` or `#f0f`). This can be `none`.
|
117
|
+
*/
|
114
118
|
protected makeIconFromPath(
|
115
119
|
pathData: string,
|
116
120
|
fill: string = 'var(--icon-color)',
|
117
121
|
strokeColor: string = 'none',
|
118
122
|
strokeWidth: string = '0px',
|
119
|
-
) {
|
123
|
+
): IconType {
|
120
124
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
121
125
|
const path = document.createElementNS(svgNamespace, 'path');
|
122
126
|
path.setAttribute('d', pathData);
|
@@ -129,7 +133,7 @@ export default class IconProvider {
|
|
129
133
|
return icon;
|
130
134
|
}
|
131
135
|
|
132
|
-
public makeHandToolIcon() {
|
136
|
+
public makeHandToolIcon(): IconType {
|
133
137
|
const fill = 'none';
|
134
138
|
const strokeColor = 'var(--icon-color)';
|
135
139
|
const strokeWidth = '3';
|
@@ -158,7 +162,7 @@ export default class IconProvider {
|
|
158
162
|
`, fill, strokeColor, strokeWidth);
|
159
163
|
}
|
160
164
|
|
161
|
-
public makeTouchPanningIcon() {
|
165
|
+
public makeTouchPanningIcon(): IconType {
|
162
166
|
const fill = 'none';
|
163
167
|
const strokeColor = 'var(--icon-color)';
|
164
168
|
const strokeWidth = '3';
|
@@ -192,7 +196,7 @@ export default class IconProvider {
|
|
192
196
|
`, fill, strokeColor, strokeWidth);
|
193
197
|
}
|
194
198
|
|
195
|
-
public makeAllDevicePanningIcon() {
|
199
|
+
public makeAllDevicePanningIcon(): IconType {
|
196
200
|
const fill = 'none';
|
197
201
|
const strokeColor = 'var(--icon-color)';
|
198
202
|
const strokeWidth = '3';
|
@@ -248,7 +252,7 @@ export default class IconProvider {
|
|
248
252
|
`, fill, strokeColor, strokeWidth);
|
249
253
|
}
|
250
254
|
|
251
|
-
public makeZoomIcon
|
255
|
+
public makeZoomIcon(): IconType {
|
252
256
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
253
257
|
icon.setAttribute('viewBox', '0 0 100 100');
|
254
258
|
|
@@ -270,9 +274,9 @@ export default class IconProvider {
|
|
270
274
|
addTextNode('-', 70, 75);
|
271
275
|
|
272
276
|
return icon;
|
273
|
-
}
|
277
|
+
}
|
274
278
|
|
275
|
-
public makeTextIcon(textStyle: TextStyle) {
|
279
|
+
public makeTextIcon(textStyle: TextStyle): IconType {
|
276
280
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
277
281
|
icon.setAttribute('viewBox', '0 0 100 100');
|
278
282
|
|
@@ -295,7 +299,7 @@ export default class IconProvider {
|
|
295
299
|
return icon;
|
296
300
|
}
|
297
301
|
|
298
|
-
public makePenIcon(tipThickness: number, color: string|Color4) {
|
302
|
+
public makePenIcon(tipThickness: number, color: string|Color4): IconType {
|
299
303
|
if (color instanceof Color4) {
|
300
304
|
color = color.toHexString();
|
301
305
|
}
|
@@ -334,7 +338,7 @@ export default class IconProvider {
|
|
334
338
|
return icon;
|
335
339
|
}
|
336
340
|
|
337
|
-
public makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory) {
|
341
|
+
public makeIconFromFactory(pen: Pen, factory: ComponentBuilderFactory): IconType {
|
338
342
|
const toolThickness = pen.getThickness();
|
339
343
|
|
340
344
|
const nowTime = (new Date()).getTime();
|
@@ -365,7 +369,7 @@ export default class IconProvider {
|
|
365
369
|
return icon;
|
366
370
|
}
|
367
371
|
|
368
|
-
public makePipetteIcon(color?: Color4) {
|
372
|
+
public makePipetteIcon(color?: Color4): IconType {
|
369
373
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
370
374
|
const pipette = document.createElementNS(svgNamespace, 'path');
|
371
375
|
|
@@ -419,7 +423,7 @@ export default class IconProvider {
|
|
419
423
|
return icon;
|
420
424
|
}
|
421
425
|
|
422
|
-
public makeResizeViewportIcon() {
|
426
|
+
public makeResizeViewportIcon(): IconType {
|
423
427
|
return this.makeIconFromPath(`
|
424
428
|
M 75 5 75 10 90 10 90 25 95 25 95 5 75 5 z
|
425
429
|
M 15 15 15 30 20 30 20 20 30 20 30 15 15 15 z
|
@@ -432,14 +436,14 @@ export default class IconProvider {
|
|
432
436
|
`);
|
433
437
|
}
|
434
438
|
|
435
|
-
public makeDuplicateSelectionIcon() {
|
439
|
+
public makeDuplicateSelectionIcon(): IconType {
|
436
440
|
return this.makeIconFromPath(`
|
437
441
|
M 45,10 45,55 90,55 90,10 45,10 z
|
438
442
|
M 10,25 10,90 70,90 70,60 40,60 40,25 10,25 z
|
439
443
|
`);
|
440
444
|
}
|
441
445
|
|
442
|
-
public makeDeleteSelectionIcon() {
|
446
|
+
public makeDeleteSelectionIcon(): IconType {
|
443
447
|
const strokeWidth = '5px';
|
444
448
|
const strokeColor = 'var(--icon-color)';
|
445
449
|
const fillColor = 'none';
|
@@ -450,7 +454,7 @@ export default class IconProvider {
|
|
450
454
|
`, fillColor, strokeColor, strokeWidth);
|
451
455
|
}
|
452
456
|
|
453
|
-
public makeSaveIcon() {
|
457
|
+
public makeSaveIcon(): IconType {
|
454
458
|
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
455
459
|
svg.innerHTML = `
|
456
460
|
<style>
|
@@ -105,12 +105,16 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
105
105
|
objectSelectLabel.innerText = this.localizationTable.selectObjectType;
|
106
106
|
objectSelectLabel.setAttribute('for', objectTypeSelect.id);
|
107
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
|
+
|
108
112
|
thicknessInput.type = 'range';
|
109
|
-
thicknessInput.min =
|
110
|
-
thicknessInput.max =
|
111
|
-
thicknessInput.step = '1';
|
113
|
+
thicknessInput.min = `${inverseThicknessInputFn(2)}`;
|
114
|
+
thicknessInput.max = `${inverseThicknessInputFn(400)}`;
|
115
|
+
thicknessInput.step = '0.1';
|
112
116
|
thicknessInput.oninput = () => {
|
113
|
-
this.tool.setThickness(parseFloat(thicknessInput.value)
|
117
|
+
this.tool.setThickness(thicknessInputFn(parseFloat(thicknessInput.value)));
|
114
118
|
};
|
115
119
|
thicknessRow.appendChild(thicknessLabel);
|
116
120
|
thicknessRow.appendChild(thicknessInput);
|
@@ -142,7 +146,7 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
142
146
|
|
143
147
|
this.updateInputs = () => {
|
144
148
|
colorInput.value = this.tool.getColor().toHexString();
|
145
|
-
thicknessInput.value =
|
149
|
+
thicknessInput.value = inverseThicknessInputFn(this.tool.getThickness()).toString();
|
146
150
|
|
147
151
|
objectTypeSelect.replaceChildren();
|
148
152
|
for (let i = 0; i < this.penTypes.length; i ++) {
|
@@ -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) {
|
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,19 @@ 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
|
+
overflow: hidden;
|
51
|
+
|
52
|
+
padding: 0;
|
53
|
+
margin: 0;
|
42
54
|
border: none;
|
43
55
|
padding: 0;
|
56
|
+
|
57
|
+
min-width: 100px;
|
58
|
+
min-height: 1.1em;
|
44
59
|
}
|
45
60
|
`);
|
46
61
|
this.editor.createHTMLOverlay(this.textEditOverlay);
|
@@ -50,7 +65,7 @@ export default class TextTool extends BaseTool {
|
|
50
65
|
private getTextAscent(text: string, style: TextStyle): number {
|
51
66
|
this.textMeasuringCtx ??= document.createElement('canvas').getContext('2d');
|
52
67
|
if (this.textMeasuringCtx) {
|
53
|
-
|
68
|
+
TextComponent.applyTextStyles(this.textMeasuringCtx, style);
|
54
69
|
return this.textMeasuringCtx.measureText(text).actualBoundingBoxAscent;
|
55
70
|
}
|
56
71
|
|
@@ -60,7 +75,7 @@ export default class TextTool extends BaseTool {
|
|
60
75
|
|
61
76
|
private flushInput() {
|
62
77
|
if (this.textInputElem && this.textTargetPosition) {
|
63
|
-
const content = this.textInputElem.value;
|
78
|
+
const content = this.textInputElem.value.trimEnd();
|
64
79
|
this.textInputElem.remove();
|
65
80
|
this.textInputElem = null;
|
66
81
|
|
@@ -70,23 +85,33 @@ export default class TextTool extends BaseTool {
|
|
70
85
|
|
71
86
|
const textTransform = Mat33.translation(
|
72
87
|
this.textTargetPosition
|
88
|
+
).rightMul(
|
89
|
+
this.getTextScaleMatrix()
|
73
90
|
).rightMul(
|
74
91
|
Mat33.scaling2D(this.editor.viewport.getSizeOfPixelOnCanvas())
|
75
92
|
).rightMul(
|
76
93
|
Mat33.zRotation(this.textRotation)
|
77
94
|
);
|
78
95
|
|
79
|
-
const textComponent =
|
80
|
-
[ content ],
|
81
|
-
textTransform,
|
82
|
-
this.textStyle,
|
83
|
-
);
|
96
|
+
const textComponent = TextComponent.fromLines(content.split('\n'), textTransform, this.textStyle);
|
84
97
|
|
85
98
|
const action = EditorImage.addElement(textComponent);
|
86
|
-
this.
|
99
|
+
if (this.removeExistingCommand) {
|
100
|
+
// Unapply so that `removeExistingCommand` can be added to the undo stack.
|
101
|
+
this.removeExistingCommand.unapply(this.editor);
|
102
|
+
|
103
|
+
this.editor.dispatch(uniteCommands([ this.removeExistingCommand, action ]));
|
104
|
+
this.removeExistingCommand = null;
|
105
|
+
} else {
|
106
|
+
this.editor.dispatch(action);
|
107
|
+
}
|
87
108
|
}
|
88
109
|
}
|
89
110
|
|
111
|
+
private getTextScaleMatrix() {
|
112
|
+
return Mat33.scaling2D(this.textScale.times(1/this.editor.viewport.getSizeOfPixelOnCanvas()));
|
113
|
+
}
|
114
|
+
|
90
115
|
private updateTextInput() {
|
91
116
|
if (!this.textInputElem || !this.textTargetPosition) {
|
92
117
|
this.textInputElem?.remove();
|
@@ -95,7 +120,6 @@ export default class TextTool extends BaseTool {
|
|
95
120
|
|
96
121
|
const viewport = this.editor.viewport;
|
97
122
|
const textScreenPos = viewport.canvasToScreen(this.textTargetPosition);
|
98
|
-
this.textInputElem.type = 'text';
|
99
123
|
this.textInputElem.placeholder = this.localizationTable.enterTextToInsert;
|
100
124
|
this.textInputElem.style.fontFamily = this.textStyle.fontFamily;
|
101
125
|
this.textInputElem.style.fontVariant = this.textStyle.fontVariant ?? '';
|
@@ -108,24 +132,34 @@ export default class TextTool extends BaseTool {
|
|
108
132
|
this.textInputElem.style.top = `${textScreenPos.y}px`;
|
109
133
|
this.textInputElem.style.margin = '0';
|
110
134
|
|
135
|
+
this.textInputElem.style.width = `${this.textInputElem.scrollWidth}px`;
|
136
|
+
this.textInputElem.style.height = `${this.textInputElem.scrollHeight}px`;
|
137
|
+
|
111
138
|
const rotation = this.textRotation + viewport.getRotationAngle();
|
112
139
|
const ascent = this.getTextAscent(this.textInputElem.value || 'W', this.textStyle);
|
113
|
-
|
140
|
+
const scale: Mat33 = this.getTextScaleMatrix();
|
141
|
+
this.textInputElem.style.transform = `${scale.toCSSMatrix()} rotate(${rotation * 180 / Math.PI}deg) translate(0, ${-ascent}px)`;
|
114
142
|
this.textInputElem.style.transformOrigin = 'top left';
|
115
143
|
}
|
116
144
|
|
117
145
|
private startTextInput(textCanvasPos: Vec2, initialText: string) {
|
118
146
|
this.flushInput();
|
119
147
|
|
120
|
-
this.textInputElem = document.createElement('
|
148
|
+
this.textInputElem = document.createElement('textarea');
|
121
149
|
this.textInputElem.value = initialText;
|
150
|
+
this.textInputElem.style.display = 'inline-block';
|
122
151
|
this.textTargetPosition = textCanvasPos;
|
123
152
|
this.textRotation = -this.editor.viewport.getRotationAngle();
|
153
|
+
this.textScale = Vec2.of(1, 1).times(this.editor.viewport.getSizeOfPixelOnCanvas());
|
124
154
|
this.updateTextInput();
|
125
155
|
|
156
|
+
// Update the input size/position/etc. after the placeHolder has had time to appear.
|
157
|
+
setTimeout(() => this.updateTextInput(), 0);
|
158
|
+
|
126
159
|
this.textInputElem.oninput = () => {
|
127
160
|
if (this.textInputElem) {
|
128
|
-
this.textInputElem.
|
161
|
+
this.textInputElem.style.width = `${this.textInputElem.scrollWidth}px`;
|
162
|
+
this.textInputElem.style.height = `${this.textInputElem.scrollHeight}px`;
|
129
163
|
}
|
130
164
|
};
|
131
165
|
this.textInputElem.onblur = () => {
|
@@ -134,7 +168,7 @@ export default class TextTool extends BaseTool {
|
|
134
168
|
setTimeout(() => this.flushInput(), 0);
|
135
169
|
};
|
136
170
|
this.textInputElem.onkeyup = (evt) => {
|
137
|
-
if (evt.key === 'Enter') {
|
171
|
+
if (evt.key === 'Enter' && !evt.shiftKey) {
|
138
172
|
this.flushInput();
|
139
173
|
this.editor.focus();
|
140
174
|
} else if (evt.key === 'Escape') {
|
@@ -142,6 +176,9 @@ export default class TextTool extends BaseTool {
|
|
142
176
|
this.textInputElem?.remove();
|
143
177
|
this.textInputElem = null;
|
144
178
|
this.editor.focus();
|
179
|
+
|
180
|
+
this.removeExistingCommand?.unapply(this.editor);
|
181
|
+
this.removeExistingCommand = null;
|
145
182
|
}
|
146
183
|
};
|
147
184
|
|
@@ -165,7 +202,33 @@ export default class TextTool extends BaseTool {
|
|
165
202
|
}
|
166
203
|
|
167
204
|
if (allPointers.length === 1) {
|
168
|
-
|
205
|
+
|
206
|
+
// Are we clicking on a text node?
|
207
|
+
const canvasPos = current.canvasPos;
|
208
|
+
const halfTestRegionSize = Vec2.of(2.5, 2.5).times(this.editor.viewport.getSizeOfPixelOnCanvas());
|
209
|
+
const testRegion = Rect2.fromCorners(canvasPos.minus(halfTestRegionSize), canvasPos.plus(halfTestRegionSize));
|
210
|
+
const targetNodes = this.editor.image.getElementsIntersectingRegion(testRegion);
|
211
|
+
const targetTextNodes = targetNodes.filter(node => node instanceof TextComponent) as TextComponent[];
|
212
|
+
|
213
|
+
if (targetTextNodes.length > 0) {
|
214
|
+
const targetNode = targetTextNodes[targetTextNodes.length - 1];
|
215
|
+
this.setTextStyle(targetNode.getTextStyle());
|
216
|
+
|
217
|
+
// Create and temporarily apply removeExistingCommand.
|
218
|
+
this.removeExistingCommand = new Erase([ targetNode ]);
|
219
|
+
this.removeExistingCommand.apply(this.editor);
|
220
|
+
|
221
|
+
this.startTextInput(targetNode.getBaselinePos(), targetNode.getText());
|
222
|
+
|
223
|
+
const transform = targetNode.getTransform();
|
224
|
+
this.textRotation = transform.transformVec3(Vec2.unitX).angle();
|
225
|
+
const scaleFactor = transform.transformVec3(Vec2.unitX).magnitude();
|
226
|
+
this.textScale = Vec2.of(1, 1).times(scaleFactor);
|
227
|
+
this.updateTextInput();
|
228
|
+
} else {
|
229
|
+
this.removeExistingCommand = null;
|
230
|
+
this.startTextInput(current.canvasPos, '');
|
231
|
+
}
|
169
232
|
return true;
|
170
233
|
}
|
171
234
|
|
@@ -224,4 +287,10 @@ export default class TextTool extends BaseTool {
|
|
224
287
|
public getTextStyle(): TextStyle {
|
225
288
|
return this.textStyle;
|
226
289
|
}
|
290
|
+
|
291
|
+
private setTextStyle(style: TextStyle) {
|
292
|
+
// Copy the style — we may change parts of it.
|
293
|
+
this.textStyle = {...style};
|
294
|
+
this.dispatchUpdateEvent();
|
295
|
+
}
|
227
296
|
}
|