js-draw 0.9.0 → 0.9.2
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.yml +742 -0
- package/CHANGELOG.md +8 -0
- package/build_tools/buildTranslationTemplate.ts +119 -0
- package/dist/build_tools/buildTranslationTemplate.d.ts +1 -0
- package/dist/build_tools/buildTranslationTemplate.js +93 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +1 -0
- package/dist/src/Editor.js +20 -3
- package/dist/src/SVGLoader.js +1 -1
- package/dist/src/Viewport.js +3 -5
- package/dist/src/commands/Command.d.ts +2 -2
- package/dist/src/commands/uniteCommands.js +19 -10
- package/dist/src/components/{Text.d.ts → TextComponent.d.ts} +0 -0
- package/dist/src/components/{Text.js → TextComponent.js} +0 -0
- package/dist/src/components/builders/LineBuilder.js +4 -0
- package/dist/src/components/lib.d.ts +1 -1
- package/dist/src/components/lib.js +1 -1
- package/dist/src/components/util/StrokeSmoother.js +1 -1
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/CanvasRenderer.js +1 -1
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.js +6 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +1 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +2 -2
- package/dist/src/toolbar/IconProvider.d.ts +1 -1
- package/dist/src/tools/FindTool.d.ts +21 -0
- package/dist/src/tools/FindTool.js +113 -0
- package/dist/src/tools/SelectionTool/Selection.js +43 -12
- package/dist/src/tools/SelectionTool/SelectionTool.js +1 -1
- package/dist/src/tools/TextTool.d.ts +1 -1
- package/dist/src/tools/TextTool.js +1 -1
- package/dist/src/tools/ToolController.js +2 -0
- package/dist/src/tools/localization.d.ts +6 -0
- package/dist/src/tools/localization.js +6 -0
- package/package.json +2 -1
- package/src/Editor.css +1 -0
- package/src/Editor.toSVG.test.ts +1 -1
- package/src/Editor.ts +27 -3
- package/src/SVGLoader.ts +1 -1
- package/src/Viewport.ts +3 -5
- package/src/commands/Command.ts +2 -2
- package/src/commands/uniteCommands.ts +21 -10
- package/src/components/{Text.test.ts → TextComponent.test.ts} +1 -1
- package/src/components/{Text.ts → TextComponent.ts} +0 -0
- package/src/components/builders/LineBuilder.ts +4 -0
- package/src/components/lib.ts +1 -1
- package/src/components/util/StrokeSmoother.ts +1 -1
- package/src/rendering/renderers/AbstractRenderer.ts +1 -1
- package/src/rendering/renderers/CanvasRenderer.ts +1 -1
- package/src/rendering/renderers/DummyRenderer.ts +1 -1
- package/src/rendering/renderers/SVGRenderer.ts +6 -2
- package/src/rendering/renderers/TextOnlyRenderer.ts +3 -3
- package/src/toolbar/IconProvider.ts +1 -1
- package/src/tools/FindTool.css +7 -0
- package/src/tools/FindTool.ts +151 -0
- package/src/tools/PasteHandler.ts +1 -1
- package/src/tools/SelectionTool/Selection.ts +52 -11
- package/src/tools/SelectionTool/SelectionTool.ts +1 -1
- package/src/tools/TextTool.ts +1 -1
- package/src/tools/ToolController.ts +2 -0
- package/src/tools/localization.ts +14 -0
- package/.firebase/hosting.ZG9jcw.cache +0 -338
- package/.github/ISSUE_TEMPLATE/translation.md +0 -100
package/src/Editor.ts
CHANGED
@@ -39,6 +39,7 @@ import { EditorLocalization } from './localization';
|
|
39
39
|
import getLocalizationTable from './localizations/getLocalizationTable';
|
40
40
|
import IconProvider from './toolbar/IconProvider';
|
41
41
|
import { toRoundedString } from './math/rounding';
|
42
|
+
import CanvasRenderer from './rendering/renderers/CanvasRenderer';
|
42
43
|
|
43
44
|
type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';
|
44
45
|
type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent)=>boolean;
|
@@ -823,21 +824,44 @@ export class Editor {
|
|
823
824
|
});
|
824
825
|
}
|
825
826
|
|
827
|
+
// Get a data URL (e.g. as produced by `HTMLCanvasElement::toDataURL`).
|
828
|
+
// If `format` is not `image/png`, a PNG image URL may still be returned (as in the
|
829
|
+
// case of `HTMLCanvasElement::toDataURL`).
|
830
|
+
//
|
831
|
+
// The export resolution is the same as the size of the drawing canvas.
|
832
|
+
public toDataURL(format: 'image/png'|'image/jpeg'|'image/webp' = 'image/png'): string {
|
833
|
+
const canvas = document.createElement('canvas');
|
834
|
+
|
835
|
+
const resolution = this.importExportViewport.getResolution();
|
836
|
+
|
837
|
+
canvas.width = resolution.x;
|
838
|
+
canvas.height = resolution.y;
|
839
|
+
|
840
|
+
const ctx = canvas.getContext('2d')!;
|
841
|
+
const renderer = new CanvasRenderer(ctx, this.importExportViewport);
|
842
|
+
|
843
|
+
this.image.renderAll(renderer);
|
844
|
+
|
845
|
+
const dataURL = canvas.toDataURL(format);
|
846
|
+
return dataURL;
|
847
|
+
}
|
848
|
+
|
826
849
|
public toSVG(): SVGElement {
|
827
850
|
const importExportViewport = this.importExportViewport;
|
828
851
|
const svgNameSpace = 'http://www.w3.org/2000/svg';
|
829
852
|
const result = document.createElementNS(svgNameSpace, 'svg');
|
830
853
|
const renderer = new SVGRenderer(result, importExportViewport);
|
831
854
|
|
832
|
-
const origTransform = importExportViewport.canvasToScreenTransform;
|
833
|
-
//
|
855
|
+
const origTransform = this.importExportViewport.canvasToScreenTransform;
|
856
|
+
// Render with (0,0) at (0,0) — we'll handle translation with
|
857
|
+
// the viewBox property.
|
834
858
|
importExportViewport.resetTransform(Mat33.identity);
|
835
859
|
|
836
|
-
// Render **all** elements.
|
837
860
|
this.image.renderAll(renderer);
|
838
861
|
|
839
862
|
importExportViewport.resetTransform(origTransform);
|
840
863
|
|
864
|
+
|
841
865
|
// Just show the main region
|
842
866
|
const rect = importExportViewport.visibleRect;
|
843
867
|
result.setAttribute('viewBox', [rect.x, rect.y, rect.w, rect.h].map(part => toRoundedString(part)).join(' '));
|
package/src/SVGLoader.ts
CHANGED
@@ -3,7 +3,7 @@ import AbstractComponent from './components/AbstractComponent';
|
|
3
3
|
import ImageComponent from './components/ImageComponent';
|
4
4
|
import Stroke from './components/Stroke';
|
5
5
|
import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
|
6
|
-
import TextComponent, { TextStyle } from './components/
|
6
|
+
import TextComponent, { TextStyle } from './components/TextComponent';
|
7
7
|
import UnknownSVGObject from './components/UnknownSVGObject';
|
8
8
|
import Mat33 from './math/Mat33';
|
9
9
|
import Path from './math/Path';
|
package/src/Viewport.ts
CHANGED
@@ -190,8 +190,8 @@ export class Viewport {
|
|
190
190
|
|
191
191
|
// Represent as k 10ⁿ for some n, k ∈ ℤ.
|
192
192
|
const decimalComponent = 10 ** Math.floor(Math.log10(Math.abs(scaleRatio)));
|
193
|
-
const
|
194
|
-
scaleRatio = Math.round(scaleRatio / decimalComponent *
|
193
|
+
const roundAmountFactor = 2 ** roundAmount;
|
194
|
+
scaleRatio = Math.round(scaleRatio / decimalComponent * roundAmountFactor) / roundAmountFactor * decimalComponent;
|
195
195
|
|
196
196
|
return scaleRatio;
|
197
197
|
}
|
@@ -223,9 +223,7 @@ export class Viewport {
|
|
223
223
|
const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 1/3;
|
224
224
|
|
225
225
|
if ((largerThanTarget && allowZoomOut) || (muchSmallerThanTarget && allowZoomIn)) {
|
226
|
-
|
227
|
-
// If smaller, shrink the visible rectangle as much as possible
|
228
|
-
const multiplier = (largerThanTarget ? Math.max : Math.min)(
|
226
|
+
const multiplier = Math.max(
|
229
227
|
toMakeVisible.w / targetRect.w, toMakeVisible.h / targetRect.h
|
230
228
|
);
|
231
229
|
const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
|
package/src/commands/Command.ts
CHANGED
@@ -2,8 +2,8 @@ import Editor from '../Editor';
|
|
2
2
|
import { EditorLocalization } from '../localization';
|
3
3
|
|
4
4
|
export abstract class Command {
|
5
|
-
public abstract apply(editor: Editor): void;
|
6
|
-
public abstract unapply(editor: Editor): void;
|
5
|
+
public abstract apply(editor: Editor): Promise<void>|void;
|
6
|
+
public abstract unapply(editor: Editor): Promise<void>|void;
|
7
7
|
|
8
8
|
// Called when the command is being deleted
|
9
9
|
public onDrop(_editor: Editor) { }
|
@@ -9,23 +9,34 @@ class NonSerializableUnion extends Command {
|
|
9
9
|
super();
|
10
10
|
}
|
11
11
|
|
12
|
+
private static waitForAll(commands: (Promise<void>|void)[]): Promise<void>|void {
|
13
|
+
// If any are Promises...
|
14
|
+
if (commands.some(command => command && command['then'])) {
|
15
|
+
console.log('waiting...');
|
16
|
+
// Wait for all commands to finish.
|
17
|
+
return Promise.all(commands)
|
18
|
+
// Ensure we return a Promise<void> and not a Promise<void[]>
|
19
|
+
.then(() => {});
|
20
|
+
}
|
21
|
+
|
22
|
+
return;
|
23
|
+
}
|
24
|
+
|
12
25
|
public apply(editor: Editor) {
|
13
26
|
if (this.applyChunkSize === undefined) {
|
14
|
-
|
15
|
-
|
16
|
-
}
|
27
|
+
const results = this.commands.map(cmd => cmd.apply(editor));
|
28
|
+
return NonSerializableUnion.waitForAll(results);
|
17
29
|
} else {
|
18
|
-
editor.asyncApplyCommands(this.commands, this.applyChunkSize);
|
30
|
+
return editor.asyncApplyCommands(this.commands, this.applyChunkSize);
|
19
31
|
}
|
20
32
|
}
|
21
33
|
|
22
34
|
public unapply(editor: Editor) {
|
23
35
|
if (this.applyChunkSize === undefined) {
|
24
|
-
|
25
|
-
|
26
|
-
}
|
36
|
+
const results = this.commands.map(cmd => cmd.unapply(editor));
|
37
|
+
return NonSerializableUnion.waitForAll(results);
|
27
38
|
} else {
|
28
|
-
editor.asyncUnapplyCommands(this.commands, this.applyChunkSize);
|
39
|
+
return editor.asyncUnapplyCommands(this.commands, this.applyChunkSize);
|
29
40
|
}
|
30
41
|
}
|
31
42
|
|
@@ -71,11 +82,11 @@ class SerializableUnion extends SerializableCommand {
|
|
71
82
|
}
|
72
83
|
|
73
84
|
public apply(editor: Editor) {
|
74
|
-
this.nonserializableCommand.apply(editor);
|
85
|
+
return this.nonserializableCommand.apply(editor);
|
75
86
|
}
|
76
87
|
|
77
88
|
public unapply(editor: Editor) {
|
78
|
-
this.nonserializableCommand.unapply(editor);
|
89
|
+
return this.nonserializableCommand.unapply(editor);
|
79
90
|
}
|
80
91
|
|
81
92
|
public description(editor: Editor, localizationTable: EditorLocalization): string {
|
@@ -1,7 +1,7 @@
|
|
1
1
|
import Color4 from '../Color4';
|
2
2
|
import Mat33 from '../math/Mat33';
|
3
3
|
import AbstractComponent from './AbstractComponent';
|
4
|
-
import TextComponent, { TextStyle } from './
|
4
|
+
import TextComponent, { TextStyle } from './TextComponent';
|
5
5
|
|
6
6
|
|
7
7
|
describe('Text', () => {
|
File without changes
|
@@ -51,6 +51,10 @@ export default class LineBuilder implements ComponentBuilder {
|
|
51
51
|
kind: PathCommandType.LineTo,
|
52
52
|
point: endPoint.minus(scaledEndNormal),
|
53
53
|
},
|
54
|
+
{
|
55
|
+
kind: PathCommandType.LineTo,
|
56
|
+
point: startPoint.minus(scaledStartNormal),
|
57
|
+
},
|
54
58
|
],
|
55
59
|
style: {
|
56
60
|
fill: this.startPoint.color,
|
package/src/components/lib.ts
CHANGED
@@ -6,7 +6,7 @@ export { default as StrokeSmoother, Curve as StrokeSmootherCurve } from './util/
|
|
6
6
|
export * from './AbstractComponent';
|
7
7
|
export { default as AbstractComponent } from './AbstractComponent';
|
8
8
|
import Stroke from './Stroke';
|
9
|
-
import TextComponent from './
|
9
|
+
import TextComponent from './TextComponent';
|
10
10
|
import ImageComponent from './ImageComponent';
|
11
11
|
|
12
12
|
export {
|
@@ -238,7 +238,7 @@ export class StrokeSmoother {
|
|
238
238
|
if (!controlPoint || segmentStart.eq(controlPoint) || segmentEnd.eq(controlPoint)) {
|
239
239
|
// Position the control point closer to the first -- the connecting
|
240
240
|
// segment will be roughly a line.
|
241
|
-
controlPoint = segmentStart.plus(enteringVec.times(startEndDist /
|
241
|
+
controlPoint = segmentStart.plus(enteringVec.times(startEndDist / 4));
|
242
242
|
}
|
243
243
|
|
244
244
|
console.assert(!segmentStart.eq(controlPoint, 1e-11), 'Start and control points are equal!');
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import { LoadSaveDataTable } from '../../components/AbstractComponent';
|
2
|
-
import { TextStyle } from '../../components/
|
2
|
+
import { TextStyle } from '../../components/TextComponent';
|
3
3
|
import Mat33 from '../../math/Mat33';
|
4
4
|
import Path, { PathCommand, PathCommandType } from '../../math/Path';
|
5
5
|
import Rect2 from '../../math/Rect2';
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import Color4 from '../../Color4';
|
2
|
-
import TextComponent, { TextStyle } from '../../components/
|
2
|
+
import TextComponent, { TextStyle } from '../../components/TextComponent';
|
3
3
|
import Mat33 from '../../math/Mat33';
|
4
4
|
import Rect2 from '../../math/Rect2';
|
5
5
|
import { Point2, Vec2 } from '../../math/Vec2';
|
@@ -1,6 +1,6 @@
|
|
1
1
|
// Renderer that outputs nothing. Useful for automated tests.
|
2
2
|
|
3
|
-
import { TextStyle } from '../../components/
|
3
|
+
import { TextStyle } from '../../components/TextComponent';
|
4
4
|
import Mat33 from '../../math/Mat33';
|
5
5
|
import Rect2 from '../../math/Rect2';
|
6
6
|
import { Point2, Vec2 } from '../../math/Vec2';
|
@@ -1,6 +1,6 @@
|
|
1
1
|
|
2
2
|
import { LoadSaveDataTable } from '../../components/AbstractComponent';
|
3
|
-
import { TextStyle } from '../../components/
|
3
|
+
import { TextStyle } from '../../components/TextComponent';
|
4
4
|
import Mat33 from '../../math/Mat33';
|
5
5
|
import Path from '../../math/Path';
|
6
6
|
import Rect2 from '../../math/Rect2';
|
@@ -94,7 +94,11 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
94
94
|
pathElem.setAttribute('d', this.lastPathString.join(' '));
|
95
95
|
|
96
96
|
const style = this.lastPathStyle;
|
97
|
-
|
97
|
+
if (style.fill.a > 0) {
|
98
|
+
pathElem.setAttribute('fill', style.fill.toHexString());
|
99
|
+
} else {
|
100
|
+
pathElem.setAttribute('fill', 'none');
|
101
|
+
}
|
98
102
|
|
99
103
|
if (style.stroke) {
|
100
104
|
pathElem.setAttribute('stroke', style.stroke.color.toHexString());
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { TextStyle } from '../../components/
|
1
|
+
import { TextStyle } from '../../components/TextComponent';
|
2
2
|
import Mat33 from '../../math/Mat33';
|
3
3
|
import Rect2 from '../../math/Rect2';
|
4
4
|
import { Vec2 } from '../../math/Vec2';
|
@@ -34,8 +34,8 @@ export default class TextOnlyRenderer extends AbstractRenderer {
|
|
34
34
|
public getDescription(): string {
|
35
35
|
return [
|
36
36
|
this.localizationTable.pathNodeCount(this.pathCount),
|
37
|
-
...(this.textNodeCount > 0 ? this.localizationTable.textNodeCount(this.textNodeCount) : []),
|
38
|
-
...(this.imageNodeCount > 0 ? this.localizationTable.imageNodeCount(this.imageNodeCount) : []),
|
37
|
+
...(this.textNodeCount > 0 ? [this.localizationTable.textNodeCount(this.textNodeCount)] : []),
|
38
|
+
...(this.imageNodeCount > 0 ? [this.localizationTable.imageNodeCount(this.imageNodeCount)] : []),
|
39
39
|
...this.descriptionBuilder
|
40
40
|
].join('\n');
|
41
41
|
}
|
@@ -1,6 +1,6 @@
|
|
1
1
|
import Color4 from '../Color4';
|
2
2
|
import { ComponentBuilderFactory } from '../components/builders/types';
|
3
|
-
import { TextStyle } from '../components/
|
3
|
+
import { TextStyle } from '../components/TextComponent';
|
4
4
|
import EventDispatcher from '../EventDispatcher';
|
5
5
|
import { Vec2 } from '../math/Vec2';
|
6
6
|
import SVGRenderer from '../rendering/renderers/SVGRenderer';
|
@@ -0,0 +1,151 @@
|
|
1
|
+
// Displays a find dialog that allows the user to search for and focus text.
|
2
|
+
//
|
3
|
+
// @packageDocumentation
|
4
|
+
|
5
|
+
import Editor from '../Editor';
|
6
|
+
import TextComponent from '../components/TextComponent';
|
7
|
+
import Rect2 from '../math/Rect2';
|
8
|
+
import { KeyPressEvent } from '../types';
|
9
|
+
import BaseTool from './BaseTool';
|
10
|
+
|
11
|
+
export const cssPrefix = 'find-tool';
|
12
|
+
|
13
|
+
export default class FindTool extends BaseTool {
|
14
|
+
private overlay: HTMLElement;
|
15
|
+
private searchInput: HTMLInputElement;
|
16
|
+
private currentMatchIdx: number = 0;
|
17
|
+
|
18
|
+
public constructor(private editor: Editor) {
|
19
|
+
super(editor.notifier, editor.localization.findLabel);
|
20
|
+
|
21
|
+
this.overlay = document.createElement('div');
|
22
|
+
this.fillOverlay();
|
23
|
+
editor.createHTMLOverlay(this.overlay);
|
24
|
+
|
25
|
+
this.overlay.style.display = 'none';
|
26
|
+
this.overlay.classList.add(`${cssPrefix}-overlay`);
|
27
|
+
}
|
28
|
+
|
29
|
+
private getMatches(searchFor: string): Rect2[] {
|
30
|
+
searchFor = searchFor.toLocaleLowerCase();
|
31
|
+
const allTextComponents = this.editor.image.getAllElements()
|
32
|
+
.filter(
|
33
|
+
elem => elem instanceof TextComponent
|
34
|
+
) as TextComponent[];
|
35
|
+
|
36
|
+
const matches = allTextComponents.filter(
|
37
|
+
text => text.getText().toLocaleLowerCase().indexOf(searchFor) !== -1
|
38
|
+
);
|
39
|
+
|
40
|
+
return matches.map(match => match.getBBox());
|
41
|
+
}
|
42
|
+
|
43
|
+
private focusCurrentMatch() {
|
44
|
+
const matches = this.getMatches(this.searchInput.value);
|
45
|
+
let matchIdx = this.currentMatchIdx % matches.length;
|
46
|
+
|
47
|
+
if (matchIdx < 0) {
|
48
|
+
matchIdx = matches.length + matchIdx;
|
49
|
+
}
|
50
|
+
|
51
|
+
if (matchIdx < matches.length) {
|
52
|
+
this.editor.dispatch(this.editor.viewport.zoomTo(matches[matchIdx], true, true));
|
53
|
+
this.editor.announceForAccessibility(
|
54
|
+
this.editor.localization.focusedFoundText(matchIdx + 1, matches.length)
|
55
|
+
);
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
private toNextMatch() {
|
60
|
+
this.currentMatchIdx ++;
|
61
|
+
this.focusCurrentMatch();
|
62
|
+
}
|
63
|
+
|
64
|
+
private toPrevMatch() {
|
65
|
+
this.currentMatchIdx --;
|
66
|
+
this.focusCurrentMatch();
|
67
|
+
}
|
68
|
+
|
69
|
+
private fillOverlay() {
|
70
|
+
const label = document.createElement('label');
|
71
|
+
this.searchInput = document.createElement('input');
|
72
|
+
const nextBtn = document.createElement('button');
|
73
|
+
const closeBtn = document.createElement('button');
|
74
|
+
|
75
|
+
// Math.random() ensures that the ID is unique (to allow us to refer to it
|
76
|
+
// with an htmlFor).
|
77
|
+
this.searchInput.setAttribute('id', `${cssPrefix}-searchInput-${Math.random()}`);
|
78
|
+
label.htmlFor = this.searchInput.getAttribute('id')!;
|
79
|
+
|
80
|
+
label.innerText = this.editor.localization.findLabel;
|
81
|
+
nextBtn.innerText = this.editor.localization.toNextMatch;
|
82
|
+
closeBtn.innerText = this.editor.localization.closeFindDialog;
|
83
|
+
|
84
|
+
this.searchInput.onkeydown = (ev: KeyboardEvent) => {
|
85
|
+
if (ev.key === 'Enter') {
|
86
|
+
if (ev.shiftKey) {
|
87
|
+
this.toPrevMatch();
|
88
|
+
} else {
|
89
|
+
this.toNextMatch();
|
90
|
+
}
|
91
|
+
}
|
92
|
+
else if (ev.key === 'Escape') {
|
93
|
+
this.setVisible(false);
|
94
|
+
}
|
95
|
+
else if (ev.key === 'f' && ev.ctrlKey) {
|
96
|
+
ev.preventDefault();
|
97
|
+
this.toggleVisible();
|
98
|
+
}
|
99
|
+
};
|
100
|
+
|
101
|
+
nextBtn.onclick = () => {
|
102
|
+
this.toNextMatch();
|
103
|
+
};
|
104
|
+
|
105
|
+
closeBtn.onclick = () => {
|
106
|
+
this.setVisible(false);
|
107
|
+
};
|
108
|
+
|
109
|
+
this.overlay.replaceChildren(label, this.searchInput, nextBtn, closeBtn);
|
110
|
+
}
|
111
|
+
|
112
|
+
private isVisible() {
|
113
|
+
return this.overlay.style.display !== 'none';
|
114
|
+
}
|
115
|
+
|
116
|
+
private setVisible(visible: boolean) {
|
117
|
+
if (visible !== this.isVisible()) {
|
118
|
+
this.overlay.style.display = visible ? 'block' : 'none';
|
119
|
+
|
120
|
+
if (visible) {
|
121
|
+
this.searchInput.focus();
|
122
|
+
this.editor.announceForAccessibility(this.editor.localization.findDialogShown);
|
123
|
+
} else {
|
124
|
+
this.editor.focus();
|
125
|
+
this.editor.announceForAccessibility(this.editor.localization.findDialogHidden);
|
126
|
+
}
|
127
|
+
}
|
128
|
+
}
|
129
|
+
|
130
|
+
private toggleVisible() {
|
131
|
+
this.setVisible(!this.isVisible());
|
132
|
+
}
|
133
|
+
|
134
|
+
public onKeyPress(event: KeyPressEvent): boolean {
|
135
|
+
if (event.ctrlKey && event.key === 'f') {
|
136
|
+
this.toggleVisible();
|
137
|
+
|
138
|
+
return true;
|
139
|
+
}
|
140
|
+
|
141
|
+
return false;
|
142
|
+
}
|
143
|
+
|
144
|
+
public setEnabled(enabled: boolean) {
|
145
|
+
super.setEnabled(enabled);
|
146
|
+
|
147
|
+
if (enabled) {
|
148
|
+
this.setVisible(false);
|
149
|
+
}
|
150
|
+
}
|
151
|
+
}
|
@@ -14,7 +14,7 @@ import EditorImage from '../EditorImage';
|
|
14
14
|
import SelectionTool from './SelectionTool/SelectionTool';
|
15
15
|
import TextTool from './TextTool';
|
16
16
|
import Color4 from '../Color4';
|
17
|
-
import { TextStyle } from '../components/
|
17
|
+
import { TextStyle } from '../components/TextComponent';
|
18
18
|
import ImageComponent from '../components/ImageComponent';
|
19
19
|
import Viewport from '../Viewport';
|
20
20
|
|
@@ -170,7 +170,7 @@ export default class Selection {
|
|
170
170
|
});
|
171
171
|
|
172
172
|
const fullTransform = this.transform;
|
173
|
-
const
|
173
|
+
const selectedElems = this.selectedElems;
|
174
174
|
|
175
175
|
// Reset for the next drag
|
176
176
|
this.transformCommands = [];
|
@@ -179,43 +179,84 @@ export default class Selection {
|
|
179
179
|
|
180
180
|
// Make the commands undo-able
|
181
181
|
this.editor.dispatch(new Selection.ApplyTransformationCommand(
|
182
|
-
this,
|
182
|
+
this, selectedElems, fullTransform
|
183
183
|
));
|
184
184
|
}
|
185
185
|
|
186
186
|
static {
|
187
|
-
SerializableCommand.register('selection-tool-transform', (json: any,
|
187
|
+
SerializableCommand.register('selection-tool-transform', (json: any, _editor) => {
|
188
188
|
// The selection box is lost when serializing/deserializing. No need to store box rotation
|
189
189
|
const fullTransform: Mat33 = new Mat33(...(json.transform as Mat33Array));
|
190
|
-
const
|
190
|
+
const elemIds: string[] = (json.elems as any[] ?? []);
|
191
191
|
|
192
|
-
return new this.ApplyTransformationCommand(null,
|
192
|
+
return new this.ApplyTransformationCommand(null, elemIds, fullTransform);
|
193
193
|
});
|
194
194
|
}
|
195
195
|
|
196
196
|
private static ApplyTransformationCommand = class extends SerializableCommand {
|
197
|
+
private transformCommands: Command[];
|
198
|
+
private selectedElemIds: string[];
|
199
|
+
|
197
200
|
public constructor(
|
198
201
|
private selection: Selection|null,
|
199
|
-
|
202
|
+
|
203
|
+
// If a `string[]`, selectedElems is a list of element IDs.
|
204
|
+
selectedElems: AbstractComponent[]|string[],
|
205
|
+
|
206
|
+
// Full transformation used to transform elements.
|
200
207
|
private fullTransform: Mat33,
|
201
208
|
) {
|
202
209
|
super('selection-tool-transform');
|
210
|
+
|
211
|
+
const isIDList = (arr: AbstractComponent[]|string[]): arr is string[] => {
|
212
|
+
return typeof arr[0] === 'string';
|
213
|
+
};
|
214
|
+
|
215
|
+
// If a list of element IDs,
|
216
|
+
if (isIDList(selectedElems)) {
|
217
|
+
this.selectedElemIds = selectedElems as string[];
|
218
|
+
} else {
|
219
|
+
this.selectedElemIds = (selectedElems as AbstractComponent[]).map(elem => elem.getId());
|
220
|
+
this.transformCommands = selectedElems.map(elem => {
|
221
|
+
return elem.transformBy(this.fullTransform);
|
222
|
+
});
|
223
|
+
}
|
224
|
+
}
|
225
|
+
|
226
|
+
private resolveToElems(editor: Editor) {
|
227
|
+
if (this.transformCommands) {
|
228
|
+
return;
|
229
|
+
}
|
230
|
+
|
231
|
+
this.transformCommands = this.selectedElemIds.map(id => {
|
232
|
+
const elem = editor.image.lookupElement(id);
|
233
|
+
|
234
|
+
if (!elem) {
|
235
|
+
throw new Error(`Unable to find element with ID, ${id}.`);
|
236
|
+
}
|
237
|
+
|
238
|
+
return elem.transformBy(this.fullTransform);
|
239
|
+
});
|
203
240
|
}
|
204
241
|
|
205
242
|
public async apply(editor: Editor) {
|
243
|
+
this.resolveToElems(editor);
|
244
|
+
|
206
245
|
this.selection?.setTransform(this.fullTransform, false);
|
207
246
|
this.selection?.updateUI();
|
208
|
-
await editor.asyncApplyCommands(this.
|
247
|
+
await editor.asyncApplyCommands(this.transformCommands, updateChunkSize);
|
209
248
|
this.selection?.setTransform(Mat33.identity, false);
|
210
249
|
this.selection?.recomputeRegion();
|
211
250
|
this.selection?.updateUI();
|
212
251
|
}
|
213
252
|
|
214
253
|
public async unapply(editor: Editor) {
|
254
|
+
this.resolveToElems(editor);
|
255
|
+
|
215
256
|
this.selection?.setTransform(this.fullTransform.inverse(), false);
|
216
257
|
this.selection?.updateUI();
|
217
258
|
|
218
|
-
await editor.asyncUnapplyCommands(this.
|
259
|
+
await editor.asyncUnapplyCommands(this.transformCommands, updateChunkSize);
|
219
260
|
this.selection?.setTransform(Mat33.identity);
|
220
261
|
this.selection?.recomputeRegion();
|
221
262
|
this.selection?.updateUI();
|
@@ -223,13 +264,13 @@ export default class Selection {
|
|
223
264
|
|
224
265
|
protected serializeToJSON() {
|
225
266
|
return {
|
226
|
-
|
267
|
+
elems: this.selectedElemIds,
|
227
268
|
transform: this.fullTransform.toArray(),
|
228
269
|
};
|
229
270
|
}
|
230
271
|
|
231
272
|
public description(_editor: Editor, localizationTable: EditorLocalization) {
|
232
|
-
return localizationTable.transformedElements(this.
|
273
|
+
return localizationTable.transformedElements(this.selectedElemIds.length);
|
233
274
|
}
|
234
275
|
};
|
235
276
|
|
@@ -453,7 +494,7 @@ export default class Selection {
|
|
453
494
|
|
454
495
|
public setSelectedObjects(objects: AbstractComponent[], bbox: Rect2) {
|
455
496
|
this.originalRegion = bbox;
|
456
|
-
this.selectedElems = objects;
|
497
|
+
this.selectedElems = objects.filter(object => object.isSelectable());
|
457
498
|
this.updateUI();
|
458
499
|
}
|
459
500
|
|
@@ -12,7 +12,7 @@ import Viewport from '../../Viewport';
|
|
12
12
|
import BaseTool from '../BaseTool';
|
13
13
|
import SVGRenderer from '../../rendering/renderers/SVGRenderer';
|
14
14
|
import Selection from './Selection';
|
15
|
-
import TextComponent from '../../components/
|
15
|
+
import TextComponent from '../../components/TextComponent';
|
16
16
|
|
17
17
|
export const cssPrefix = 'selection-tool-';
|
18
18
|
|
package/src/tools/TextTool.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import Color4 from '../Color4';
|
2
|
-
import TextComponent, { TextStyle } from '../components/
|
2
|
+
import TextComponent, { TextStyle } from '../components/TextComponent';
|
3
3
|
import Editor from '../Editor';
|
4
4
|
import EditorImage from '../EditorImage';
|
5
5
|
import Rect2 from '../math/Rect2';
|
@@ -15,6 +15,7 @@ import ToolSwitcherShortcut from './ToolSwitcherShortcut';
|
|
15
15
|
import PasteHandler from './PasteHandler';
|
16
16
|
import ToolbarShortcutHandler from './ToolbarShortcutHandler';
|
17
17
|
import { makePressureSensitiveFreehandLineBuilder } from '../components/builders/PressureSensitiveFreehandLineBuilder';
|
18
|
+
import FindTool from './FindTool';
|
18
19
|
|
19
20
|
export default class ToolController {
|
20
21
|
private tools: BaseTool[];
|
@@ -50,6 +51,7 @@ export default class ToolController {
|
|
50
51
|
new UndoRedoShortcut(editor),
|
51
52
|
new ToolbarShortcutHandler(editor),
|
52
53
|
new ToolSwitcherShortcut(editor),
|
54
|
+
new FindTool(editor),
|
53
55
|
new PasteHandler(editor),
|
54
56
|
];
|
55
57
|
primaryTools.forEach(tool => tool.setToolGroup(primaryToolGroup));
|
@@ -15,6 +15,13 @@ export interface ToolLocalization {
|
|
15
15
|
changeTool: string;
|
16
16
|
pasteHandler: string;
|
17
17
|
|
18
|
+
findLabel: string;
|
19
|
+
toNextMatch: string;
|
20
|
+
closeFindDialog: string;
|
21
|
+
findDialogShown: string;
|
22
|
+
findDialogHidden: string;
|
23
|
+
focusedFoundText: (currentMatchNumber: number, totalMatches: number)=> string;
|
24
|
+
|
18
25
|
anyDevicePanning: string;
|
19
26
|
|
20
27
|
copied: (count: number, description: string) => string;
|
@@ -40,6 +47,13 @@ export const defaultToolLocalization: ToolLocalization = {
|
|
40
47
|
changeTool: 'Change tool',
|
41
48
|
pasteHandler: 'Copy paste handler',
|
42
49
|
|
50
|
+
findLabel: 'Find',
|
51
|
+
toNextMatch: 'Next',
|
52
|
+
closeFindDialog: 'Close',
|
53
|
+
findDialogShown: 'Find dialog shown',
|
54
|
+
findDialogHidden: 'Find dialog hidden',
|
55
|
+
focusedFoundText: (matchIdx: number, totalMatches: number) => `Viewing match ${matchIdx} of ${totalMatches}`,
|
56
|
+
|
43
57
|
anyDevicePanning: 'Any device panning',
|
44
58
|
|
45
59
|
copied: (count: number, description: string) => `Copied ${count} ${description}`,
|