js-draw 0.1.9 → 0.1.10
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/CHANGELOG.md +6 -0
- package/README.md +15 -3
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +1 -0
- package/dist/src/Editor.js +28 -15
- package/dist/src/UndoRedoHistory.js +3 -0
- package/dist/src/bundle/bundled.d.ts +2 -1
- package/dist/src/bundle/bundled.js +2 -1
- package/dist/src/geometry/LineSegment2.d.ts +1 -0
- package/dist/src/geometry/LineSegment2.js +16 -0
- package/dist/src/geometry/Rect2.d.ts +1 -0
- package/dist/src/geometry/Rect2.js +16 -0
- package/dist/src/localizations/en.d.ts +3 -0
- package/dist/src/localizations/en.js +4 -0
- package/dist/src/localizations/es.d.ts +3 -0
- package/dist/src/localizations/es.js +18 -0
- package/dist/src/localizations/getLocalizationTable.d.ts +3 -0
- package/dist/src/localizations/getLocalizationTable.js +43 -0
- package/dist/src/toolbar/HTMLToolbar.js +5 -7
- package/dist/src/toolbar/icons.js +13 -13
- package/dist/src/toolbar/localization.d.ts +1 -0
- package/dist/src/toolbar/localization.js +1 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +29 -0
- package/dist/src/tools/BaseTool.d.ts +2 -1
- package/dist/src/tools/BaseTool.js +3 -0
- package/dist/src/tools/PanZoom.d.ts +2 -1
- package/dist/src/tools/PanZoom.js +4 -0
- package/dist/src/tools/SelectionTool.d.ts +9 -2
- package/dist/src/tools/SelectionTool.js +131 -19
- package/dist/src/tools/ToolController.js +6 -2
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/tools/localization.js +1 -0
- package/dist/src/types.d.ts +8 -2
- package/dist/src/types.js +1 -0
- package/package.json +9 -1
- package/src/Editor.ts +31 -14
- package/src/UndoRedoHistory.ts +4 -0
- package/src/bundle/bundled.ts +2 -1
- package/src/geometry/LineSegment2.test.ts +15 -0
- package/src/geometry/LineSegment2.ts +20 -0
- package/src/geometry/Rect2.test.ts +20 -7
- package/src/geometry/Rect2.ts +19 -1
- package/src/localizations/en.ts +8 -0
- package/src/localizations/es.ts +60 -0
- package/src/localizations/getLocalizationTable.test.ts +27 -0
- package/src/localizations/getLocalizationTable.ts +53 -0
- package/src/toolbar/HTMLToolbar.ts +5 -8
- package/src/toolbar/icons.ts +13 -13
- package/src/toolbar/localization.ts +2 -0
- package/src/toolbar/toolbar.css +5 -0
- package/src/toolbar/widgets/BaseWidget.ts +34 -0
- package/src/tools/BaseTool.ts +5 -1
- package/src/tools/PanZoom.ts +6 -0
- package/src/tools/SelectionTool.test.ts +24 -1
- package/src/tools/SelectionTool.ts +158 -23
- package/src/tools/ToolController.ts +6 -2
- package/src/tools/localization.ts +2 -0
- package/src/types.ts +9 -1
- package/dist-test/test-dist-bundle.html +0 -42
@@ -0,0 +1,27 @@
|
|
1
|
+
|
2
|
+
import { defaultEditorLocalization } from '../localization';
|
3
|
+
import en from './en';
|
4
|
+
import es from './es';
|
5
|
+
import getLocalizationTable from './getLocalizationTable';
|
6
|
+
|
7
|
+
describe('getLocalizationTable', () => {
|
8
|
+
it('should return the en localization for es_TEST', () => {
|
9
|
+
expect(getLocalizationTable([ 'es_TEST' ]) === es).toBe(true);
|
10
|
+
});
|
11
|
+
|
12
|
+
it('should return the default localization for unsupported language', () => {
|
13
|
+
expect(getLocalizationTable([ 'test' ]) === defaultEditorLocalization).toBe(true);
|
14
|
+
});
|
15
|
+
|
16
|
+
it('should return the first localization matching a language in the list of user locales', () => {
|
17
|
+
expect(getLocalizationTable([ 'test_TEST1', 'test_TEST2', 'test_TEST3', 'en_TEST', 'notalanguage']) === en).toBe(true);
|
18
|
+
});
|
19
|
+
|
20
|
+
it('should return the default localization for unsupported language', () => {
|
21
|
+
expect(getLocalizationTable([ 'test' ]) === defaultEditorLocalization).toBe(true);
|
22
|
+
});
|
23
|
+
|
24
|
+
it('should return first of user\'s supported languages', () => {
|
25
|
+
expect(getLocalizationTable([ 'es_MX', 'es_ES', 'en_US' ]) === es).toBe(true);
|
26
|
+
});
|
27
|
+
});
|
@@ -0,0 +1,53 @@
|
|
1
|
+
|
2
|
+
import { defaultEditorLocalization, EditorLocalization } from '../localization';
|
3
|
+
import en from './en';
|
4
|
+
import es from './es';
|
5
|
+
|
6
|
+
const allLocales: Record<string, EditorLocalization> = {
|
7
|
+
en,
|
8
|
+
es,
|
9
|
+
};
|
10
|
+
|
11
|
+
// [locale]: A string in the format languageCode_Region or just languageCode. For example, en_US.
|
12
|
+
const languageFromLocale = (locale: string) => {
|
13
|
+
const matches = /^(\w+)[_-](\w+)$/.exec(locale);
|
14
|
+
if (!matches) {
|
15
|
+
// If not in languageCode_region format, the locale should be the
|
16
|
+
// languageCode. Return that.
|
17
|
+
return locale;
|
18
|
+
}
|
19
|
+
|
20
|
+
return matches[1];
|
21
|
+
};
|
22
|
+
|
23
|
+
const getLocalizationTable = (userLocales?: readonly string[]): EditorLocalization => {
|
24
|
+
userLocales ??= navigator.languages;
|
25
|
+
|
26
|
+
let prevLanguage: string|undefined;
|
27
|
+
for (const locale of userLocales) {
|
28
|
+
const language = languageFromLocale(locale);
|
29
|
+
|
30
|
+
// If the specific localization of the language is not available, but
|
31
|
+
// a localization for the language is,
|
32
|
+
if (prevLanguage && language !== prevLanguage) {
|
33
|
+
if (prevLanguage in allLocales) {
|
34
|
+
return allLocales[prevLanguage];
|
35
|
+
}
|
36
|
+
}
|
37
|
+
|
38
|
+
// If the full locale (e.g. en_US) is available,
|
39
|
+
if (locale in allLocales) {
|
40
|
+
return allLocales[locale];
|
41
|
+
}
|
42
|
+
|
43
|
+
prevLanguage = language;
|
44
|
+
}
|
45
|
+
|
46
|
+
if (prevLanguage && prevLanguage in allLocales) {
|
47
|
+
return allLocales[prevLanguage];
|
48
|
+
} else {
|
49
|
+
return defaultEditorLocalization;
|
50
|
+
}
|
51
|
+
};
|
52
|
+
|
53
|
+
export default getLocalizationTable;
|
@@ -138,13 +138,13 @@ export default class HTMLToolbar {
|
|
138
138
|
undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
|
139
139
|
|
140
140
|
const undoButton = this.addActionButton({
|
141
|
-
label:
|
141
|
+
label: this.localizationTable.undo,
|
142
142
|
icon: makeUndoIcon()
|
143
143
|
}, () => {
|
144
144
|
this.editor.history.undo();
|
145
145
|
}, undoRedoGroup);
|
146
146
|
const redoButton = this.addActionButton({
|
147
|
-
label:
|
147
|
+
label: this.localizationTable.redo,
|
148
148
|
icon: makeRedoIcon(),
|
149
149
|
}, () => {
|
150
150
|
this.editor.history.redo();
|
@@ -200,12 +200,9 @@ export default class HTMLToolbar {
|
|
200
200
|
(new TextToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
201
201
|
}
|
202
202
|
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
}
|
207
|
-
|
208
|
-
(new HandToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
203
|
+
const panZoomTool = toolController.getMatchingTools(ToolType.PanZoom)[0];
|
204
|
+
if (panZoomTool && panZoomTool instanceof PanZoom) {
|
205
|
+
(new HandToolWidget(this.editor, panZoomTool, this.localizationTable)).addTo(this.container);
|
209
206
|
}
|
210
207
|
|
211
208
|
this.setupColorPickers();
|
package/src/toolbar/icons.ts
CHANGED
@@ -9,11 +9,11 @@ import { StrokeDataPoint } from '../types';
|
|
9
9
|
import Viewport from '../Viewport';
|
10
10
|
|
11
11
|
const svgNamespace = 'http://www.w3.org/2000/svg';
|
12
|
-
const
|
13
|
-
style='fill: var(--
|
12
|
+
const iconColorFill = `
|
13
|
+
style='fill: var(--icon-color);'
|
14
14
|
`;
|
15
|
-
const
|
16
|
-
style='fill: var(--
|
15
|
+
const iconColorStrokeFill = `
|
16
|
+
style='fill: var(--icon-color); stroke: var(--icon-color);'
|
17
17
|
`;
|
18
18
|
const checkerboardPatternDef = `
|
19
19
|
<pattern
|
@@ -39,7 +39,7 @@ export const makeRedoIcon = (mirror: boolean = false) => {
|
|
39
39
|
icon.innerHTML = `
|
40
40
|
<style>
|
41
41
|
.toolbar-svg-undo-redo-icon {
|
42
|
-
stroke: var(--
|
42
|
+
stroke: var(--icon-color);
|
43
43
|
stroke-width: 12;
|
44
44
|
stroke-linejoin: round;
|
45
45
|
stroke-linecap: round;
|
@@ -63,7 +63,7 @@ export const makeDropdownIcon = () => {
|
|
63
63
|
<g>
|
64
64
|
<path
|
65
65
|
d='M5,10 L50,90 L95,10 Z'
|
66
|
-
${
|
66
|
+
${iconColorFill}
|
67
67
|
/>
|
68
68
|
</g>
|
69
69
|
`;
|
@@ -80,7 +80,7 @@ export const makeEraserIcon = () => {
|
|
80
80
|
<rect x=10 y=50 width=80 height=30 rx=10 fill='pink' />
|
81
81
|
<rect
|
82
82
|
x=10 y=10 width=80 height=50
|
83
|
-
${
|
83
|
+
${iconColorFill}
|
84
84
|
/>
|
85
85
|
</g>
|
86
86
|
`;
|
@@ -132,7 +132,7 @@ export const makeHandToolIcon = () => {
|
|
132
132
|
|
133
133
|
fill='none'
|
134
134
|
style='
|
135
|
-
stroke: var(--
|
135
|
+
stroke: var(--icon-color);
|
136
136
|
stroke-width: 2;
|
137
137
|
'
|
138
138
|
/>
|
@@ -175,7 +175,7 @@ export const makeTouchPanningIcon = () => {
|
|
175
175
|
'
|
176
176
|
fill='none'
|
177
177
|
style='
|
178
|
-
stroke: var(--
|
178
|
+
stroke: var(--icon-color);
|
179
179
|
stroke-width: 2;
|
180
180
|
'
|
181
181
|
/>
|
@@ -241,7 +241,7 @@ export const makeAllDevicePanningIcon = () => {
|
|
241
241
|
'
|
242
242
|
fill='none'
|
243
243
|
style='
|
244
|
-
stroke: var(--
|
244
|
+
stroke: var(--icon-color);
|
245
245
|
stroke-width: 2;
|
246
246
|
'
|
247
247
|
/>
|
@@ -263,7 +263,7 @@ export const makeZoomIcon = () => {
|
|
263
263
|
textNode.style.textAlign = 'center';
|
264
264
|
textNode.style.textAnchor = 'middle';
|
265
265
|
textNode.style.fontSize = '55px';
|
266
|
-
textNode.style.fill = 'var(--
|
266
|
+
textNode.style.fill = 'var(--icon-color)';
|
267
267
|
textNode.style.fontFamily = 'monospace';
|
268
268
|
|
269
269
|
icon.appendChild(textNode);
|
@@ -315,7 +315,7 @@ export const makePenIcon = (tipThickness: number, color: string) => {
|
|
315
315
|
<!-- Pen grip -->
|
316
316
|
<path
|
317
317
|
d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z'
|
318
|
-
${
|
318
|
+
${iconColorStrokeFill}
|
319
319
|
/>
|
320
320
|
</g>
|
321
321
|
<g>
|
@@ -389,7 +389,7 @@ export const makePipetteIcon = (color?: Color4) => {
|
|
389
389
|
65,15 65,5 47,6
|
390
390
|
Z
|
391
391
|
`);
|
392
|
-
pipette.style.fill = 'var(--
|
392
|
+
pipette.style.fill = 'var(--icon-color)';
|
393
393
|
|
394
394
|
if (color) {
|
395
395
|
const defs = document.createElementNS(svgNamespace, 'defs');
|
@@ -23,6 +23,7 @@ export interface ToolbarLocalization {
|
|
23
23
|
undo: string;
|
24
24
|
redo: string;
|
25
25
|
zoom: string;
|
26
|
+
selectionToolKeyboardShortcuts: string;
|
26
27
|
|
27
28
|
dropdownShown: (toolName: string)=> string;
|
28
29
|
dropdownHidden: (toolName: string)=> string;
|
@@ -46,6 +47,7 @@ export const defaultToolbarLocalization: ToolbarLocalization = {
|
|
46
47
|
redo: 'Redo',
|
47
48
|
selectObjectType: 'Object type: ',
|
48
49
|
pickColorFronScreen: 'Pick color from screen',
|
50
|
+
selectionToolKeyboardShortcuts: 'Selection tool: Use arrow keys to move selected items, lowercase/uppercase ‘i’ and ‘o’ to resize.',
|
49
51
|
|
50
52
|
touchPanning: 'Touchscreen panning',
|
51
53
|
anyDevicePanning: 'Any device panning',
|
package/src/toolbar/toolbar.css
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
.toolbar-root {
|
2
2
|
background-color: var(--primary-background-color);
|
3
|
+
--icon-color: var(--primary-foreground-color);
|
4
|
+
|
3
5
|
|
4
6
|
border: 1px solid var(--secondary-background-color);
|
5
7
|
border-radius: 2px;
|
@@ -38,6 +40,7 @@
|
|
38
40
|
text-align: center;
|
39
41
|
border-radius: 6px;
|
40
42
|
|
43
|
+
--icon-color: var(--primary-foreground-color);
|
41
44
|
background-color: var(--primary-background-color);
|
42
45
|
color: var(--primary-foreground-color);
|
43
46
|
border: none;
|
@@ -83,6 +86,7 @@
|
|
83
86
|
.toolbar-toolContainer.selected .toolbar-button {
|
84
87
|
background-color: var(--secondary-background-color);
|
85
88
|
color: var(--secondary-foreground-color);
|
89
|
+
--icon-color: var(--secondary-foreground-color);
|
86
90
|
}
|
87
91
|
|
88
92
|
.toolbar-toolContainer:not(.selected):not(.dropdownShowable) > .toolbar-button > .toolbar-showHideDropdownIcon {
|
@@ -173,4 +177,5 @@
|
|
173
177
|
|
174
178
|
.color-input-container .pipetteButton.active {
|
175
179
|
background-color: var(--secondary-background-color);
|
180
|
+
--icon-color: var(--secondary-foreground-color);
|
176
181
|
}
|
@@ -1,4 +1,5 @@
|
|
1
1
|
import Editor from '../../Editor';
|
2
|
+
import { InputEvtType } from '../../types';
|
2
3
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
3
4
|
import { makeDropdownIcon } from '../icons';
|
4
5
|
import { ToolbarLocalization } from '../localization';
|
@@ -50,6 +51,39 @@ export default abstract class BaseWidget {
|
|
50
51
|
}
|
51
52
|
|
52
53
|
protected setupActionBtnClickListener(button: HTMLElement) {
|
54
|
+
const clickTriggers = { enter: true, ' ': true, };
|
55
|
+
button.onkeydown = (evt) => {
|
56
|
+
let handled = false;
|
57
|
+
|
58
|
+
if (evt.key in clickTriggers) {
|
59
|
+
if (!this.disabled) {
|
60
|
+
this.handleClick();
|
61
|
+
handled = true;
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
// If we didn't do anything with the event, send it to the editor.
|
66
|
+
if (!handled) {
|
67
|
+
this.editor.toolController.dispatchInputEvent({
|
68
|
+
kind: InputEvtType.KeyPressEvent,
|
69
|
+
key: evt.key,
|
70
|
+
ctrlKey: evt.ctrlKey,
|
71
|
+
});
|
72
|
+
}
|
73
|
+
};
|
74
|
+
|
75
|
+
button.onkeyup = evt => {
|
76
|
+
if (evt.key in clickTriggers) {
|
77
|
+
return;
|
78
|
+
}
|
79
|
+
|
80
|
+
this.editor.toolController.dispatchInputEvent({
|
81
|
+
kind: InputEvtType.KeyUpEvent,
|
82
|
+
key: evt.key,
|
83
|
+
ctrlKey: evt.ctrlKey,
|
84
|
+
});
|
85
|
+
};
|
86
|
+
|
53
87
|
button.onclick = () => {
|
54
88
|
if (!this.disabled) {
|
55
89
|
this.handleClick();
|
package/src/tools/BaseTool.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent } from '../types';
|
1
|
+
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent, KeyUpEvent } from '../types';
|
2
2
|
import { ToolType } from './ToolController';
|
3
3
|
import ToolEnabledGroup from './ToolEnabledGroup';
|
4
4
|
|
@@ -24,6 +24,10 @@ export default abstract class BaseTool implements PointerEvtListener {
|
|
24
24
|
return false;
|
25
25
|
}
|
26
26
|
|
27
|
+
public onKeyUp(_event: KeyUpEvent): boolean {
|
28
|
+
return false;
|
29
|
+
}
|
30
|
+
|
27
31
|
public setEnabled(enabled: boolean) {
|
28
32
|
this.enabled = enabled;
|
29
33
|
|
package/src/tools/PanZoom.ts
CHANGED
@@ -16,11 +16,13 @@ interface PinchData {
|
|
16
16
|
dist: number;
|
17
17
|
}
|
18
18
|
|
19
|
+
|
19
20
|
export enum PanZoomMode {
|
20
21
|
OneFingerTouchGestures = 0x1,
|
21
22
|
TwoFingerTouchGestures = 0x1 << 1,
|
22
23
|
RightClickDrags = 0x1 << 2,
|
23
24
|
SinglePointerGestures = 0x1 << 3,
|
25
|
+
Keyboard = 0x1 << 4,
|
24
26
|
}
|
25
27
|
|
26
28
|
export default class PanZoom extends BaseTool {
|
@@ -180,6 +182,10 @@ export default class PanZoom extends BaseTool {
|
|
180
182
|
}
|
181
183
|
|
182
184
|
public onKeyPress({ key }: KeyPressEvent): boolean {
|
185
|
+
if (!(this.mode & PanZoomMode.Keyboard)) {
|
186
|
+
return false;
|
187
|
+
}
|
188
|
+
|
183
189
|
let translation = Vec2.zero;
|
184
190
|
let scale = 1;
|
185
191
|
let rotation = 0;
|
@@ -66,7 +66,7 @@ describe('SelectionTool', () => {
|
|
66
66
|
|
67
67
|
// Drag the object
|
68
68
|
selection!.handleBackgroundDrag(Vec2.of(5, 5));
|
69
|
-
selection!.
|
69
|
+
selection!.finalizeTransform();
|
70
70
|
|
71
71
|
expect(testStroke.getBBox().topLeft).toMatchObject({
|
72
72
|
x: 5,
|
@@ -80,4 +80,27 @@ describe('SelectionTool', () => {
|
|
80
80
|
y: 0,
|
81
81
|
});
|
82
82
|
});
|
83
|
+
|
84
|
+
it('moving the selection with a keyboard should move the view to keep the selection in view', () => {
|
85
|
+
const { addTestStrokeCommand } = createSquareStroke();
|
86
|
+
const editor = createEditor();
|
87
|
+
editor.dispatch(addTestStrokeCommand);
|
88
|
+
|
89
|
+
// Select the stroke
|
90
|
+
const selectionTool = getSelectionTool(editor);
|
91
|
+
selectionTool.setEnabled(true);
|
92
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
|
93
|
+
editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10, 10));
|
94
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(100, 100));
|
95
|
+
|
96
|
+
const selection = selectionTool.getSelection();
|
97
|
+
if (selection === null) {
|
98
|
+
// Throw to allow TypeScript's non-null checker to understand that selection
|
99
|
+
// must be non-null after this.
|
100
|
+
throw new Error('Selection should be non-null.');
|
101
|
+
}
|
102
|
+
|
103
|
+
selection.handleBackgroundDrag(Vec2.of(0, -1000));
|
104
|
+
expect(editor.viewport.visibleRect.containsPoint(selection.region.center)).toBe(true);
|
105
|
+
});
|
83
106
|
});
|
@@ -8,7 +8,8 @@ import Mat33 from '../geometry/Mat33';
|
|
8
8
|
import Rect2 from '../geometry/Rect2';
|
9
9
|
import { Point2, Vec2 } from '../geometry/Vec2';
|
10
10
|
import { EditorLocalization } from '../localization';
|
11
|
-
import { EditorEventType, PointerEvt } from '../types';
|
11
|
+
import { EditorEventType, KeyPressEvent, KeyUpEvent, PointerEvt } from '../types';
|
12
|
+
import Viewport from '../Viewport';
|
12
13
|
import BaseTool from './BaseTool';
|
13
14
|
import { ToolType } from './ToolController';
|
14
15
|
|
@@ -163,15 +164,15 @@ class Selection {
|
|
163
164
|
|
164
165
|
makeDraggable(draggableBackground, (deltaPosition: Vec2) => {
|
165
166
|
this.handleBackgroundDrag(deltaPosition);
|
166
|
-
}, () => this.
|
167
|
+
}, () => this.finalizeTransform());
|
167
168
|
|
168
169
|
makeDraggable(resizeCorner, (deltaPosition) => {
|
169
170
|
this.handleResizeCornerDrag(deltaPosition);
|
170
|
-
}, () => this.
|
171
|
+
}, () => this.finalizeTransform());
|
171
172
|
|
172
173
|
makeDraggable(this.rotateCircle, (_deltaPosition, offset) => {
|
173
174
|
this.handleRotateCircleDrag(offset);
|
174
|
-
}, () => this.
|
175
|
+
}, () => this.finalizeTransform());
|
175
176
|
}
|
176
177
|
|
177
178
|
// Note a small change in the position of this' background while dragging
|
@@ -186,10 +187,7 @@ class Selection {
|
|
186
187
|
// Snap position to a multiple of 10 (additional decimal points lead to larger files).
|
187
188
|
deltaPosition = this.editor.viewport.roundPoint(deltaPosition);
|
188
189
|
|
189
|
-
this.
|
190
|
-
this.transform = this.transform.rightMul(Mat33.translation(deltaPosition));
|
191
|
-
|
192
|
-
this.previewTransformCmds();
|
190
|
+
this.transformPreview(Mat33.translation(deltaPosition));
|
193
191
|
}
|
194
192
|
|
195
193
|
public handleResizeCornerDrag(deltaPosition: Vec2) {
|
@@ -203,21 +201,13 @@ class Selection {
|
|
203
201
|
const newSize = this.region.size.plus(deltaPosition);
|
204
202
|
|
205
203
|
if (newSize.y > 0 && newSize.x > 0) {
|
206
|
-
|
207
|
-
const scaleFactor = Vec2.of(this.region.w / oldWidth, this.region.h / oldHeight);
|
204
|
+
const scaleFactor = Vec2.of(newSize.x / oldWidth, newSize.y / oldHeight);
|
208
205
|
|
209
|
-
|
210
|
-
this.transform = this.transform.rightMul(currentTransfm);
|
211
|
-
this.previewTransformCmds();
|
206
|
+
this.transformPreview(Mat33.scaling2D(scaleFactor, this.region.topLeft));
|
212
207
|
}
|
213
208
|
}
|
214
209
|
|
215
210
|
public handleRotateCircleDrag(offset: Vec2) {
|
216
|
-
this.boxRotation = this.boxRotation % (2 * Math.PI);
|
217
|
-
if (this.boxRotation < 0) {
|
218
|
-
this.boxRotation += 2 * Math.PI;
|
219
|
-
}
|
220
|
-
|
221
211
|
let targetRotation = offset.angle();
|
222
212
|
targetRotation = targetRotation % (2 * Math.PI);
|
223
213
|
if (targetRotation < 0) {
|
@@ -237,9 +227,7 @@ class Selection {
|
|
237
227
|
deltaRotation *= rotationDirection;
|
238
228
|
}
|
239
229
|
|
240
|
-
this.
|
241
|
-
this.boxRotation += deltaRotation;
|
242
|
-
this.previewTransformCmds();
|
230
|
+
this.transformPreview(Mat33.zRotation(deltaRotation, this.region.center));
|
243
231
|
}
|
244
232
|
|
245
233
|
private computeTransformCommands() {
|
@@ -248,8 +236,29 @@ class Selection {
|
|
248
236
|
});
|
249
237
|
}
|
250
238
|
|
239
|
+
// Applies, previews, but doesn't finalize the given transformation.
|
240
|
+
public transformPreview(transform: Mat33) {
|
241
|
+
this.transform = this.transform.rightMul(transform);
|
242
|
+
const deltaRotation = transform.transformVec3(Vec2.unitX).angle();
|
243
|
+
transform = transform.rightMul(Mat33.zRotation(-deltaRotation, this.region.center));
|
244
|
+
|
245
|
+
this.boxRotation += deltaRotation;
|
246
|
+
this.boxRotation = this.boxRotation % (2 * Math.PI);
|
247
|
+
if (this.boxRotation < 0) {
|
248
|
+
this.boxRotation += 2 * Math.PI;
|
249
|
+
}
|
250
|
+
|
251
|
+
const newSize = transform.transformVec3(this.region.size);
|
252
|
+
const translation = transform.transformVec2(this.region.topLeft).minus(this.region.topLeft);
|
253
|
+
this.region = this.region.resizedTo(newSize);
|
254
|
+
this.region = this.region.translatedBy(translation);
|
255
|
+
|
256
|
+
this.previewTransformCmds();
|
257
|
+
this.scrollTo();
|
258
|
+
}
|
259
|
+
|
251
260
|
// Applies the current transformation to the selection
|
252
|
-
public
|
261
|
+
public finalizeTransform() {
|
253
262
|
this.transformationCommands.forEach(cmd => {
|
254
263
|
cmd.unapply(this.editor);
|
255
264
|
});
|
@@ -321,7 +330,6 @@ class Selection {
|
|
321
330
|
this.updateUI();
|
322
331
|
}
|
323
332
|
|
324
|
-
|
325
333
|
public appendBackgroundBoxTo(elem: HTMLElement) {
|
326
334
|
if (this.backgroundBox.parentElement) {
|
327
335
|
this.backgroundBox.remove();
|
@@ -444,6 +452,19 @@ class Selection {
|
|
444
452
|
this.rotateCircle.style.transform = `rotate(${-rotationDeg}deg)`;
|
445
453
|
}
|
446
454
|
|
455
|
+
// Scroll the viewport to this. Does not zoom
|
456
|
+
public scrollTo() {
|
457
|
+
const viewport = this.editor.viewport;
|
458
|
+
const visibleRect = viewport.visibleRect;
|
459
|
+
if (!visibleRect.containsPoint(this.region.center)) {
|
460
|
+
const closestPoint = visibleRect.getClosestPointOnBoundaryTo(this.region.center);
|
461
|
+
const delta = this.region.center.minus(closestPoint);
|
462
|
+
this.editor.dispatchNoAnnounce(
|
463
|
+
new Viewport.ViewportTransform(Mat33.translation(delta.times(-1))), false
|
464
|
+
);
|
465
|
+
}
|
466
|
+
}
|
467
|
+
|
447
468
|
public deleteSelectedObjects(): Command {
|
448
469
|
return new Erase(this.selectedElems);
|
449
470
|
}
|
@@ -473,6 +494,8 @@ export default class SelectionTool extends BaseTool {
|
|
473
494
|
this.selectionBox?.recomputeRegion();
|
474
495
|
this.selectionBox?.updateUI();
|
475
496
|
});
|
497
|
+
|
498
|
+
this.editor.handleKeyEventsFrom(this.handleOverlay);
|
476
499
|
}
|
477
500
|
|
478
501
|
public onPointerDown(event: PointerEvt): boolean {
|
@@ -512,7 +535,12 @@ export default class SelectionTool extends BaseTool {
|
|
512
535
|
this.editor.announceForAccessibility(
|
513
536
|
this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount())
|
514
537
|
);
|
538
|
+
this.zoomToSelection();
|
539
|
+
}
|
540
|
+
}
|
515
541
|
|
542
|
+
private zoomToSelection() {
|
543
|
+
if (this.selectionBox) {
|
516
544
|
const selectionRect = this.selectionBox.region;
|
517
545
|
this.editor.dispatchNoAnnounce(this.editor.viewport.zoomTo(selectionRect, false), false);
|
518
546
|
}
|
@@ -532,6 +560,106 @@ export default class SelectionTool extends BaseTool {
|
|
532
560
|
this.selectionBox?.appendBackgroundBoxTo(this.handleOverlay);
|
533
561
|
}
|
534
562
|
|
563
|
+
private static handleableKeys = [
|
564
|
+
'a', 'h', 'ArrowLeft',
|
565
|
+
'd', 'l', 'ArrowRight',
|
566
|
+
'q', 'k', 'ArrowUp',
|
567
|
+
'e', 'j', 'ArrowDown',
|
568
|
+
'r', 'R',
|
569
|
+
'i', 'I', 'o', 'O',
|
570
|
+
];
|
571
|
+
public onKeyPress(event: KeyPressEvent): boolean {
|
572
|
+
let rotationSteps = 0;
|
573
|
+
let xTranslateSteps = 0;
|
574
|
+
let yTranslateSteps = 0;
|
575
|
+
let xScaleSteps = 0;
|
576
|
+
let yScaleSteps = 0;
|
577
|
+
|
578
|
+
switch (event.key) {
|
579
|
+
case 'a':
|
580
|
+
case 'h':
|
581
|
+
case 'ArrowLeft':
|
582
|
+
xTranslateSteps -= 1;
|
583
|
+
break;
|
584
|
+
case 'd':
|
585
|
+
case 'l':
|
586
|
+
case 'ArrowRight':
|
587
|
+
xTranslateSteps += 1;
|
588
|
+
break;
|
589
|
+
case 'q':
|
590
|
+
case 'k':
|
591
|
+
case 'ArrowUp':
|
592
|
+
yTranslateSteps -= 1;
|
593
|
+
break;
|
594
|
+
case 'e':
|
595
|
+
case 'j':
|
596
|
+
case 'ArrowDown':
|
597
|
+
yTranslateSteps += 1;
|
598
|
+
break;
|
599
|
+
case 'r':
|
600
|
+
rotationSteps += 1;
|
601
|
+
break;
|
602
|
+
case 'R':
|
603
|
+
rotationSteps -= 1;
|
604
|
+
break;
|
605
|
+
case 'i':
|
606
|
+
xScaleSteps -= 1;
|
607
|
+
break;
|
608
|
+
case 'I':
|
609
|
+
xScaleSteps += 1;
|
610
|
+
break;
|
611
|
+
case 'o':
|
612
|
+
yScaleSteps -= 1;
|
613
|
+
break;
|
614
|
+
case 'O':
|
615
|
+
yScaleSteps += 1;
|
616
|
+
break;
|
617
|
+
}
|
618
|
+
|
619
|
+
let handled = xTranslateSteps !== 0
|
620
|
+
|| yTranslateSteps !== 0
|
621
|
+
|| rotationSteps !== 0
|
622
|
+
|| xScaleSteps !== 0
|
623
|
+
|| yScaleSteps !== 0;
|
624
|
+
|
625
|
+
if (!this.selectionBox) {
|
626
|
+
handled = false;
|
627
|
+
} else if (handled) {
|
628
|
+
const translateStepSize = 10 * this.editor.viewport.getSizeOfPixelOnCanvas();
|
629
|
+
const rotateStepSize = Math.PI / 8;
|
630
|
+
const scaleStepSize = translateStepSize / 2;
|
631
|
+
|
632
|
+
const region = this.selectionBox.region;
|
633
|
+
const scaledSize = this.selectionBox.region.size.plus(
|
634
|
+
Vec2.of(xScaleSteps, yScaleSteps).times(scaleStepSize)
|
635
|
+
);
|
636
|
+
|
637
|
+
const transform = Mat33.scaling2D(
|
638
|
+
Vec2.of(
|
639
|
+
// Don't more-than-half the size of the selection
|
640
|
+
Math.max(0.5, scaledSize.x / region.size.x),
|
641
|
+
Math.max(0.5, scaledSize.y / region.size.y)
|
642
|
+
),
|
643
|
+
region.topLeft
|
644
|
+
).rightMul(Mat33.zRotation(
|
645
|
+
rotationSteps * rotateStepSize, region.center
|
646
|
+
)).rightMul(Mat33.translation(
|
647
|
+
Vec2.of(xTranslateSteps, yTranslateSteps).times(translateStepSize)
|
648
|
+
));
|
649
|
+
this.selectionBox.transformPreview(transform);
|
650
|
+
}
|
651
|
+
|
652
|
+
return handled;
|
653
|
+
}
|
654
|
+
|
655
|
+
public onKeyUp(evt: KeyUpEvent) {
|
656
|
+
if (this.selectionBox && SelectionTool.handleableKeys.some(key => key === evt.key)) {
|
657
|
+
this.selectionBox.finalizeTransform();
|
658
|
+
return true;
|
659
|
+
}
|
660
|
+
return false;
|
661
|
+
}
|
662
|
+
|
535
663
|
public setEnabled(enabled: boolean) {
|
536
664
|
super.setEnabled(enabled);
|
537
665
|
|
@@ -540,6 +668,13 @@ export default class SelectionTool extends BaseTool {
|
|
540
668
|
this.selectionBox = null;
|
541
669
|
|
542
670
|
this.handleOverlay.style.display = enabled ? 'block' : 'none';
|
671
|
+
|
672
|
+
if (enabled) {
|
673
|
+
this.handleOverlay.tabIndex = 0;
|
674
|
+
this.handleOverlay.ariaLabel = this.editor.localization.selectionToolKeyboardShortcuts;
|
675
|
+
} else {
|
676
|
+
this.handleOverlay.tabIndex = -1;
|
677
|
+
}
|
543
678
|
}
|
544
679
|
|
545
680
|
// Get the object responsible for displaying this' selection.
|
@@ -30,6 +30,7 @@ export default class ToolController {
|
|
30
30
|
public constructor(editor: Editor, localization: ToolLocalization) {
|
31
31
|
const primaryToolEnabledGroup = new ToolEnabledGroup();
|
32
32
|
const panZoomTool = new PanZoom(editor, PanZoomMode.TwoFingerTouchGestures | PanZoomMode.RightClickDrags, localization.touchPanTool);
|
33
|
+
const keyboardPanZoomTool = new PanZoom(editor, PanZoomMode.Keyboard, localization.keyboardPanZoom);
|
33
34
|
const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness: 16 });
|
34
35
|
const primaryTools = [
|
35
36
|
new SelectionTool(editor, localization.selectionTool),
|
@@ -48,6 +49,7 @@ export default class ToolController {
|
|
48
49
|
new PipetteTool(editor, localization.pipetteTool),
|
49
50
|
panZoomTool,
|
50
51
|
...primaryTools,
|
52
|
+
keyboardPanZoomTool,
|
51
53
|
new UndoRedoShortcut(editor),
|
52
54
|
];
|
53
55
|
primaryTools.forEach(tool => tool.setToolGroup(primaryToolEnabledGroup));
|
@@ -88,9 +90,10 @@ export default class ToolController {
|
|
88
90
|
this.activeTool = null;
|
89
91
|
handled = true;
|
90
92
|
} else if (
|
91
|
-
event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent
|
93
|
+
event.kind === InputEvtType.WheelEvt || event.kind === InputEvtType.KeyPressEvent || event.kind === InputEvtType.KeyUpEvent
|
92
94
|
) {
|
93
95
|
const isKeyPressEvt = event.kind === InputEvtType.KeyPressEvent;
|
96
|
+
const isKeyReleaseEvt = event.kind === InputEvtType.KeyUpEvent;
|
94
97
|
const isWheelEvt = event.kind === InputEvtType.WheelEvt;
|
95
98
|
for (const tool of this.tools) {
|
96
99
|
if (!tool.isEnabled()) {
|
@@ -99,7 +102,8 @@ export default class ToolController {
|
|
99
102
|
|
100
103
|
const wheelResult = isWheelEvt && tool.onWheel(event);
|
101
104
|
const keyPressResult = isKeyPressEvt && tool.onKeyPress(event);
|
102
|
-
|
105
|
+
const keyReleaseResult = isKeyReleaseEvt && tool.onKeyUp(event);
|
106
|
+
handled = keyPressResult || wheelResult || keyReleaseResult;
|
103
107
|
|
104
108
|
if (handled) {
|
105
109
|
break;
|