js-draw 0.24.1 → 0.25.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/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Pointer.js +5 -3
- package/dist/cjs/localizations/de.js +1 -1
- package/dist/cjs/localizations/es.js +1 -1
- package/dist/cjs/rendering/renderers/CanvasRenderer.js +2 -0
- package/dist/cjs/rendering/renderers/SVGRenderer.d.ts +1 -1
- package/dist/cjs/rendering/renderers/SVGRenderer.js +4 -2
- package/dist/cjs/testing/getUniquePointerId.d.ts +4 -0
- package/dist/cjs/testing/getUniquePointerId.js +16 -0
- package/dist/cjs/testing/sendPenEvent.d.ts +1 -1
- package/dist/cjs/testing/sendPenEvent.js +4 -1
- package/dist/cjs/testing/sendTouchEvent.js +2 -9
- package/dist/cjs/toolbar/IconProvider.d.ts +1 -1
- package/dist/cjs/toolbar/IconProvider.js +76 -10
- package/dist/cjs/toolbar/localization.d.ts +2 -2
- package/dist/cjs/toolbar/localization.js +2 -2
- package/dist/cjs/toolbar/widgets/BaseToolWidget.d.ts +2 -0
- package/dist/cjs/toolbar/widgets/BaseToolWidget.js +7 -0
- package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +3 -1
- package/dist/cjs/toolbar/widgets/PenToolWidget.js +125 -41
- package/dist/cjs/toolbar/widgets/SelectionToolWidget.js +4 -0
- package/dist/cjs/tools/BaseTool.d.ts +17 -1
- package/dist/cjs/tools/BaseTool.js +18 -0
- package/dist/cjs/tools/Pen.d.ts +5 -2
- package/dist/cjs/tools/Pen.js +37 -4
- package/dist/cjs/tools/ToolController.js +14 -2
- package/dist/mjs/Pointer.mjs +5 -3
- package/dist/mjs/localizations/de.mjs +1 -1
- package/dist/mjs/localizations/es.mjs +1 -1
- package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +2 -0
- package/dist/mjs/rendering/renderers/SVGRenderer.d.ts +1 -1
- package/dist/mjs/rendering/renderers/SVGRenderer.mjs +4 -2
- package/dist/mjs/testing/getUniquePointerId.d.ts +4 -0
- package/dist/mjs/testing/getUniquePointerId.mjs +14 -0
- package/dist/mjs/testing/sendPenEvent.d.ts +1 -1
- package/dist/mjs/testing/sendPenEvent.mjs +4 -1
- package/dist/mjs/testing/sendTouchEvent.mjs +2 -9
- package/dist/mjs/toolbar/IconProvider.d.ts +1 -1
- package/dist/mjs/toolbar/IconProvider.mjs +76 -10
- package/dist/mjs/toolbar/localization.d.ts +2 -2
- package/dist/mjs/toolbar/localization.mjs +2 -2
- package/dist/mjs/toolbar/widgets/BaseToolWidget.d.ts +2 -0
- package/dist/mjs/toolbar/widgets/BaseToolWidget.mjs +7 -0
- package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +3 -1
- package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +125 -41
- package/dist/mjs/toolbar/widgets/SelectionToolWidget.mjs +4 -0
- package/dist/mjs/tools/BaseTool.d.ts +17 -1
- package/dist/mjs/tools/BaseTool.mjs +18 -0
- package/dist/mjs/tools/Pen.d.ts +5 -2
- package/dist/mjs/tools/Pen.mjs +37 -4
- package/dist/mjs/tools/ToolController.mjs +14 -2
- package/package.json +2 -2
- package/src/Pointer.ts +5 -2
- package/src/localizations/de.ts +2 -2
- package/src/localizations/es.ts +1 -1
- package/src/rendering/renderers/CanvasRenderer.ts +2 -0
- package/src/rendering/renderers/SVGRenderer.ts +6 -3
- package/src/testing/getUniquePointerId.ts +18 -0
- package/src/testing/sendPenEvent.ts +6 -1
- package/src/testing/sendTouchEvent.ts +2 -9
- package/src/toolbar/IconProvider.ts +92 -23
- package/src/toolbar/localization.ts +4 -4
- package/src/toolbar/toolbar.css +1 -0
- package/src/toolbar/widgets/BaseToolWidget.ts +10 -1
- package/src/toolbar/widgets/PenToolWidget.css +53 -0
- package/src/toolbar/widgets/PenToolWidget.ts +156 -44
- package/src/toolbar/widgets/SelectionToolWidget.ts +4 -0
- package/src/tools/BaseTool.ts +22 -1
- package/src/tools/Pen.test.ts +68 -0
- package/src/tools/Pen.ts +42 -4
- package/src/tools/ToolController.ts +17 -2
@@ -31,6 +31,9 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
31
31
|
private updateInputs: ()=> void = () => {};
|
32
32
|
protected penTypes: PenTypeRecord[];
|
33
33
|
|
34
|
+
// A counter variable that ensures different HTML elements are given unique names/ids.
|
35
|
+
private static idCounter: number = 0;
|
36
|
+
|
34
37
|
public constructor(
|
35
38
|
editor: Editor, private tool: Pen, localization?: ToolbarLocalization
|
36
39
|
) {
|
@@ -39,13 +42,13 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
39
42
|
// Default pen types
|
40
43
|
this.penTypes = [
|
41
44
|
{
|
42
|
-
name: this.localizationTable.
|
45
|
+
name: this.localizationTable.flatTipPen,
|
43
46
|
id: 'pressure-sensitive-pen',
|
44
47
|
|
45
48
|
factory: makePressureSensitiveFreehandLineBuilder,
|
46
49
|
},
|
47
50
|
{
|
48
|
-
name: this.localizationTable.
|
51
|
+
name: this.localizationTable.roundedTipPen,
|
49
52
|
id: 'freehand-pen',
|
50
53
|
|
51
54
|
factory: makeFreehandLineBuilder,
|
@@ -123,43 +126,173 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
123
126
|
return null;
|
124
127
|
}
|
125
128
|
|
126
|
-
|
127
|
-
const
|
128
|
-
|
129
|
+
private createIconForRecord(record: PenTypeRecord|null) {
|
130
|
+
const color = this.tool.getColor();
|
131
|
+
|
132
|
+
const strokeFactory = record?.factory;
|
133
|
+
if (!strokeFactory || strokeFactory === makeFreehandLineBuilder || strokeFactory === makePressureSensitiveFreehandLineBuilder) {
|
129
134
|
// Use a square-root scale to prevent the pen's tip from overflowing.
|
130
135
|
const scale = Math.round(Math.sqrt(this.tool.getThickness()) * 4);
|
131
|
-
const color = this.tool.getColor();
|
132
136
|
const roundedTip = strokeFactory === makeFreehandLineBuilder;
|
133
137
|
|
134
138
|
return this.editor.icons.makePenIcon(scale, color.toHexString(), roundedTip);
|
135
139
|
} else {
|
136
|
-
const
|
137
|
-
return this.editor.icons.makeIconFromFactory(this.tool, strokeFactory);
|
140
|
+
const hasTransparency = color.a < 1;
|
141
|
+
return this.editor.icons.makeIconFromFactory(this.tool, strokeFactory, hasTransparency);
|
138
142
|
}
|
139
143
|
}
|
140
144
|
|
141
|
-
|
145
|
+
protected createIcon(): Element {
|
146
|
+
return this.createIconForRecord(this.getCurrentPenType());
|
147
|
+
}
|
148
|
+
|
149
|
+
|
150
|
+
// Creates a widget that allows selecting different pen types
|
151
|
+
private createPenTypeSelector() {
|
152
|
+
const outerContainer = document.createElement('div');
|
153
|
+
outerContainer.classList.add(`${toolbarCSSPrefix}pen-type-selector`);
|
154
|
+
|
155
|
+
const scrollingContainer = document.createElement('div');
|
156
|
+
scrollingContainer.setAttribute('role', 'menu');
|
157
|
+
scrollingContainer.id = `${toolbarCSSPrefix}-pen-type-selector-id-${PenToolWidget.idCounter++}`;
|
158
|
+
|
159
|
+
scrollingContainer.onwheel = (event) => {
|
160
|
+
const hasScroll = scrollingContainer.clientWidth !== scrollingContainer.scrollWidth
|
161
|
+
&& event.deltaX !== 0;
|
162
|
+
const eventScrollsPastLeft =
|
163
|
+
scrollingContainer.scrollLeft + event.deltaX <= 0;
|
164
|
+
const scrollRight = scrollingContainer.scrollLeft + scrollingContainer.clientWidth;
|
165
|
+
const eventScrollsPastRight =
|
166
|
+
scrollRight + event.deltaX > scrollingContainer.scrollWidth;
|
167
|
+
|
168
|
+
// Stop the editor from receiving the event if it will scroll the pen type selector
|
169
|
+
// instead.
|
170
|
+
if (hasScroll && !eventScrollsPastLeft && !eventScrollsPastRight) {
|
171
|
+
event.stopPropagation();
|
172
|
+
}
|
173
|
+
};
|
174
|
+
|
175
|
+
const label = document.createElement('label');
|
176
|
+
label.innerText = this.localizationTable.selectPenType;
|
177
|
+
label.htmlFor = scrollingContainer.id;
|
178
|
+
outerContainer.appendChild(label);
|
179
|
+
|
180
|
+
// All buttons in a radiogroup need the same name attribute.
|
181
|
+
const radiogroupName = `${toolbarCSSPrefix}-pen-type-selector-${PenToolWidget.idCounter++}`;
|
182
|
+
|
183
|
+
const createTypeSelectorButton = (record: PenTypeRecord) => {
|
184
|
+
const buttonContainer = document.createElement('div');
|
185
|
+
buttonContainer.classList.add('pen-type-button');
|
186
|
+
|
187
|
+
const button = document.createElement('input');
|
188
|
+
button.type = 'radio';
|
189
|
+
button.name = radiogroupName;
|
190
|
+
button.id = `${toolbarCSSPrefix}-pen-type-button-${PenToolWidget.idCounter++}`;
|
191
|
+
|
192
|
+
const labelContainer = document.createElement('label');
|
193
|
+
|
194
|
+
const rebuildLabel = () => {
|
195
|
+
const labelText = document.createElement('span');
|
196
|
+
|
197
|
+
const icon = this.createIconForRecord(record);
|
198
|
+
icon.classList.add('icon');
|
199
|
+
|
200
|
+
// The title of the record
|
201
|
+
labelText.innerText = record.name;
|
202
|
+
labelContainer.htmlFor = button.id;
|
203
|
+
|
204
|
+
labelContainer.replaceChildren(icon, labelText);
|
205
|
+
};
|
206
|
+
rebuildLabel();
|
207
|
+
|
208
|
+
const updateButtonCSS = () => {
|
209
|
+
if (button.checked) {
|
210
|
+
buttonContainer.classList.add('checked');
|
211
|
+
} else {
|
212
|
+
buttonContainer.classList.remove('checked');
|
213
|
+
}
|
214
|
+
};
|
215
|
+
|
216
|
+
button.oninput = () => {
|
217
|
+
// Setting the stroke factory fires an event that causes the value
|
218
|
+
// of this button to be set.
|
219
|
+
if (button.checked) {
|
220
|
+
this.tool.setStrokeFactory(record.factory);
|
221
|
+
}
|
222
|
+
|
223
|
+
updateButtonCSS();
|
224
|
+
};
|
225
|
+
|
226
|
+
buttonContainer.replaceChildren(button, labelContainer);
|
227
|
+
scrollingContainer.appendChild(buttonContainer);
|
228
|
+
|
229
|
+
// Set whether the button is checked, assuming the stroke factory associated
|
230
|
+
// with the button was set elsewhere.
|
231
|
+
const setChecked = (checked: boolean) => {
|
232
|
+
button.checked = checked;
|
233
|
+
updateButtonCSS();
|
234
|
+
|
235
|
+
if (checked) {
|
236
|
+
button.scrollIntoView();
|
237
|
+
}
|
238
|
+
};
|
239
|
+
setChecked(false);
|
240
|
+
|
241
|
+
// Updates the factory's icon based on the current style of the tool.
|
242
|
+
const updateIcon = () => {
|
243
|
+
rebuildLabel();
|
244
|
+
};
|
245
|
+
|
246
|
+
return { setChecked, updateIcon };
|
247
|
+
};
|
248
|
+
|
249
|
+
const buttons: Array<ReturnType<typeof createTypeSelectorButton>> = [];
|
250
|
+
for (const penType of this.penTypes) {
|
251
|
+
buttons.push(createTypeSelectorButton(penType));
|
252
|
+
}
|
253
|
+
// invariant: buttons.length = this.penTypes.length
|
254
|
+
|
255
|
+
outerContainer.appendChild(scrollingContainer);
|
256
|
+
|
257
|
+
return {
|
258
|
+
setValue: (penTypeIndex: number) => {
|
259
|
+
// Select the value specified
|
260
|
+
if (penTypeIndex < 0 || penTypeIndex >= this.penTypes.length) {
|
261
|
+
console.error('Invalid pen type index', penTypeIndex);
|
262
|
+
return;
|
263
|
+
}
|
264
|
+
|
265
|
+
for (let i = 0; i < buttons.length; i++) {
|
266
|
+
buttons[i].setChecked(i === penTypeIndex);
|
267
|
+
}
|
268
|
+
},
|
269
|
+
|
270
|
+
updateIcons: () => {
|
271
|
+
buttons.forEach(button => button.updateIcon());
|
272
|
+
},
|
273
|
+
|
274
|
+
addTo: (parent: HTMLElement) => {
|
275
|
+
parent.appendChild(outerContainer);
|
276
|
+
},
|
277
|
+
};
|
278
|
+
}
|
279
|
+
|
142
280
|
protected override fillDropdown(dropdown: HTMLElement): boolean {
|
143
281
|
const container = document.createElement('div');
|
144
282
|
container.classList.add(`${toolbarCSSPrefix}spacedList`);
|
145
283
|
|
146
284
|
const thicknessRow = document.createElement('div');
|
147
|
-
const objectTypeRow = document.createElement('div');
|
148
285
|
|
149
286
|
// Thickness: Value of the input is squared to allow for finer control/larger values.
|
150
287
|
const thicknessLabel = document.createElement('label');
|
151
288
|
const thicknessInput = document.createElement('input');
|
152
|
-
const
|
153
|
-
const objectTypeSelect = document.createElement('select');
|
289
|
+
const penTypeSelect = this.createPenTypeSelector();
|
154
290
|
|
155
291
|
// Give inputs IDs so we can label them with a <label for=...>Label text</label>
|
156
292
|
thicknessInput.id = `${toolbarCSSPrefix}penThicknessInput${PenToolWidget.idCounter++}`;
|
157
|
-
objectTypeSelect.id = `${toolbarCSSPrefix}penBuilderSelect${PenToolWidget.idCounter++}`;
|
158
293
|
|
159
294
|
thicknessLabel.innerText = this.localizationTable.thicknessLabel;
|
160
295
|
thicknessLabel.setAttribute('for', thicknessInput.id);
|
161
|
-
objectSelectLabel.innerText = this.localizationTable.selectPenType;
|
162
|
-
objectSelectLabel.setAttribute('for', objectTypeSelect.id);
|
163
296
|
|
164
297
|
// Use a logarithmic scale for thicknessInput (finer control over thinner strokewidths.)
|
165
298
|
const inverseThicknessInputFn = (t: number) => Math.log10(t);
|
@@ -175,18 +308,6 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
175
308
|
thicknessRow.appendChild(thicknessLabel);
|
176
309
|
thicknessRow.appendChild(thicknessInput);
|
177
310
|
|
178
|
-
objectTypeSelect.oninput = () => {
|
179
|
-
const penTypeIdx = parseInt(objectTypeSelect.value);
|
180
|
-
if (penTypeIdx < 0 || penTypeIdx >= this.penTypes.length) {
|
181
|
-
console.error('Invalid pen type index', penTypeIdx);
|
182
|
-
return;
|
183
|
-
}
|
184
|
-
|
185
|
-
this.tool.setStrokeFactory(this.penTypes[penTypeIdx].factory);
|
186
|
-
};
|
187
|
-
objectTypeRow.appendChild(objectSelectLabel);
|
188
|
-
objectTypeRow.appendChild(objectTypeSelect);
|
189
|
-
|
190
311
|
const colorRow = document.createElement('div');
|
191
312
|
const colorLabel = document.createElement('label');
|
192
313
|
const [ colorInput, colorInputContainer, setColorInputValue ] = makeColorInput(this.editor, color => {
|
@@ -204,28 +325,15 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
204
325
|
setColorInputValue(this.tool.getColor());
|
205
326
|
thicknessInput.value = inverseThicknessInputFn(this.tool.getThickness()).toString();
|
206
327
|
|
207
|
-
|
208
|
-
objectTypeSelect.replaceChildren();
|
209
|
-
for (let i = 0; i < this.penTypes.length; i ++) {
|
210
|
-
const penType = this.penTypes[i];
|
211
|
-
const option = document.createElement('option');
|
212
|
-
option.value = i.toString();
|
213
|
-
option.innerText = penType.name;
|
214
|
-
|
215
|
-
objectTypeSelect.appendChild(option);
|
216
|
-
}
|
328
|
+
penTypeSelect.updateIcons();
|
217
329
|
|
218
330
|
// Update the selected stroke factory.
|
219
|
-
|
220
|
-
if (strokeFactoryIdx === -1) {
|
221
|
-
objectTypeSelect.value = '';
|
222
|
-
} else {
|
223
|
-
objectTypeSelect.value = strokeFactoryIdx.toString();
|
224
|
-
}
|
331
|
+
penTypeSelect.setValue(this.getCurrentPenTypeIdx());
|
225
332
|
};
|
226
333
|
this.updateInputs();
|
227
334
|
|
228
|
-
container.replaceChildren(colorRow, thicknessRow
|
335
|
+
container.replaceChildren(colorRow, thicknessRow);
|
336
|
+
penTypeSelect.addTo(container);
|
229
337
|
dropdown.replaceChildren(container);
|
230
338
|
return true;
|
231
339
|
}
|
@@ -246,6 +354,10 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
246
354
|
}
|
247
355
|
}
|
248
356
|
|
357
|
+
// Run any default actions registered by the parent class.
|
358
|
+
if (super.onKeyPress(event)) {
|
359
|
+
return true;
|
360
|
+
}
|
249
361
|
return false;
|
250
362
|
}
|
251
363
|
|
@@ -180,6 +180,10 @@ export default class SelectionToolWidget extends BaseToolWidget {
|
|
180
180
|
return true;
|
181
181
|
}
|
182
182
|
|
183
|
+
// If we didn't handle the event, allow the superclass to handle it.
|
184
|
+
if (super.onKeyPress(event)) {
|
185
|
+
return true;
|
186
|
+
}
|
183
187
|
return false;
|
184
188
|
}
|
185
189
|
|
package/src/tools/BaseTool.ts
CHANGED
@@ -5,9 +5,21 @@ export default abstract class BaseTool implements PointerEvtListener {
|
|
5
5
|
private enabled: boolean = true;
|
6
6
|
private group: ToolEnabledGroup|null = null;
|
7
7
|
|
8
|
+
/**
|
9
|
+
* Returns true iff the tool handled the event and thus should receive additional
|
10
|
+
* events.
|
11
|
+
*/
|
8
12
|
public onPointerDown(_event: PointerEvt): boolean { return false; }
|
9
13
|
public onPointerMove(_event: PointerEvt) { }
|
10
|
-
|
14
|
+
|
15
|
+
/**
|
16
|
+
* Returns true iff there are additional pointers down and the tool should
|
17
|
+
* remain active to handle the additional events.
|
18
|
+
*
|
19
|
+
* For most purposes, this should return `false` or nothing.
|
20
|
+
*/
|
21
|
+
public onPointerUp(_event: PointerEvt): boolean|void { }
|
22
|
+
|
11
23
|
public onGestureCancel() { }
|
12
24
|
|
13
25
|
protected constructor(private notifier: EditorNotifier, public readonly description: string) {
|
@@ -33,6 +45,15 @@ export default abstract class BaseTool implements PointerEvtListener {
|
|
33
45
|
return false;
|
34
46
|
}
|
35
47
|
|
48
|
+
/**
|
49
|
+
* Return true if, while this tool is active, `_event` can be delivered to
|
50
|
+
* another tool that is higher priority than this.
|
51
|
+
* @internal May be renamed
|
52
|
+
*/
|
53
|
+
public eventCanBeDeliveredToNonActiveTool(_event: PointerEvt) {
|
54
|
+
return true;
|
55
|
+
}
|
56
|
+
|
36
57
|
public setEnabled(enabled: boolean) {
|
37
58
|
this.enabled = enabled;
|
38
59
|
|
package/src/tools/Pen.test.ts
CHANGED
@@ -8,6 +8,7 @@ import StrokeComponent from '../components/Stroke';
|
|
8
8
|
import Mat33 from '../math/Mat33';
|
9
9
|
import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
|
10
10
|
import sendPenEvent from '../testing/sendPenEvent';
|
11
|
+
import sendTouchEvent from '../testing/sendTouchEvent';
|
11
12
|
|
12
13
|
describe('Pen', () => {
|
13
14
|
it('should draw horizontal lines', () => {
|
@@ -153,6 +154,73 @@ describe('Pen', () => {
|
|
153
154
|
expect(elems[0].getBBox().bottomRight).objEq(Vec2.of(420, 340), 25); // ± 25
|
154
155
|
});
|
155
156
|
|
157
|
+
// if `mainEventIsPen` is false, tests with touch events.
|
158
|
+
const testEventCancelation = (mainEventIsPen: boolean) => {
|
159
|
+
const editor = createEditor();
|
160
|
+
|
161
|
+
expect(editor.image.getElementsIntersectingRegion(new Rect2(0, 0, 1000, 1000))).toHaveLength(0);
|
162
|
+
|
163
|
+
const sendMainEvent = mainEventIsPen ? sendPenEvent : sendTouchEvent;
|
164
|
+
|
165
|
+
// Start the drawing
|
166
|
+
const mainPointer = sendMainEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(417, 24));
|
167
|
+
jest.advanceTimersByTime(245);
|
168
|
+
sendMainEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 197));
|
169
|
+
jest.advanceTimersByTime(20);
|
170
|
+
sendMainEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 199));
|
171
|
+
jest.advanceTimersByTime(12);
|
172
|
+
sendMainEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 201));
|
173
|
+
jest.advanceTimersByTime(40);
|
174
|
+
sendMainEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(423, 203));
|
175
|
+
jest.advanceTimersByTime(14);
|
176
|
+
|
177
|
+
// Attempt to cancel the drawing
|
178
|
+
let firstPointer = sendTouchEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(0, 0), [ mainPointer ]);
|
179
|
+
let secondPointer = sendTouchEvent(editor, InputEvtType.PointerDownEvt, Vec2.of(100, 0), [ firstPointer, mainPointer ]);
|
180
|
+
|
181
|
+
const maxIterations = 10;
|
182
|
+
for (let i = 0; i < maxIterations; i++) {
|
183
|
+
jest.advanceTimersByTime(100);
|
184
|
+
|
185
|
+
const point1 = Vec2.of(-i * 5, 0);
|
186
|
+
const point2 = Vec2.of(i * 5 + 100, 0);
|
187
|
+
|
188
|
+
const eventType = InputEvtType.PointerMoveEvt;
|
189
|
+
firstPointer = sendTouchEvent(editor, eventType, point1, [ secondPointer, mainPointer ]);
|
190
|
+
secondPointer = sendTouchEvent(editor, eventType, point2, [ firstPointer, mainPointer ]);
|
191
|
+
|
192
|
+
if (i === maxIterations - 1) {
|
193
|
+
jest.advanceTimersByTime(10);
|
194
|
+
|
195
|
+
sendTouchEvent(editor, InputEvtType.PointerUpEvt, point1, [ secondPointer, mainPointer ]);
|
196
|
+
sendTouchEvent(editor, InputEvtType.PointerUpEvt, point2, [ mainPointer ]);
|
197
|
+
}
|
198
|
+
|
199
|
+
jest.advanceTimersByTime(100);
|
200
|
+
}
|
201
|
+
|
202
|
+
// Finish the drawing
|
203
|
+
sendMainEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(420, 333), [firstPointer, secondPointer]);
|
204
|
+
jest.advanceTimersByTime(8);
|
205
|
+
sendMainEvent(editor, InputEvtType.PointerMoveEvt, Vec2.of(420, 340), [firstPointer, secondPointer]);
|
206
|
+
sendMainEvent(editor, InputEvtType.PointerUpEvt, Vec2.of(420, 340), [firstPointer, secondPointer]);
|
207
|
+
|
208
|
+
const elementsInDrawingArea = editor.image.getElementsIntersectingRegion(new Rect2(0, 0, 1000, 1000));
|
209
|
+
if (mainEventIsPen) {
|
210
|
+
expect(elementsInDrawingArea).toHaveLength(1);
|
211
|
+
} else {
|
212
|
+
expect(elementsInDrawingArea).toHaveLength(0);
|
213
|
+
}
|
214
|
+
};
|
215
|
+
|
216
|
+
it('pen events should not be cancelable by touch events', () => {
|
217
|
+
testEventCancelation(true);
|
218
|
+
});
|
219
|
+
|
220
|
+
it('touch events should be cancelable by touch events', () => {
|
221
|
+
testEventCancelation(false);
|
222
|
+
});
|
223
|
+
|
156
224
|
it('ctrl+z should finalize then undo the current stroke', async () => {
|
157
225
|
const editor = createEditor();
|
158
226
|
|
package/src/tools/Pen.ts
CHANGED
@@ -20,6 +20,7 @@ export default class Pen extends BaseTool {
|
|
20
20
|
protected builder: ComponentBuilder|null = null;
|
21
21
|
private lastPoint: StrokeDataPoint|null = null;
|
22
22
|
private startPoint: StrokeDataPoint|null = null;
|
23
|
+
private currentDeviceType: PointerDevice|null = null;
|
23
24
|
|
24
25
|
private snapToGridEnabled: boolean = false;
|
25
26
|
private angleLockEnabled: boolean = false;
|
@@ -94,7 +95,8 @@ export default class Pen extends BaseTool {
|
|
94
95
|
this.previewStroke();
|
95
96
|
}
|
96
97
|
|
97
|
-
public override onPointerDown(
|
98
|
+
public override onPointerDown(event: PointerEvt): boolean {
|
99
|
+
const { current, allPointers } = event;
|
98
100
|
const isEraser = current.device === PointerDevice.Eraser;
|
99
101
|
|
100
102
|
let anyDeviceIsStylus = false;
|
@@ -105,24 +107,57 @@ export default class Pen extends BaseTool {
|
|
105
107
|
}
|
106
108
|
}
|
107
109
|
|
110
|
+
// Avoid canceling an existing stroke
|
111
|
+
if (this.builder && !this.eventCanCancelStroke(event)) {
|
112
|
+
return true;
|
113
|
+
}
|
114
|
+
|
108
115
|
if ((allPointers.length === 1 && !isEraser) || anyDeviceIsStylus) {
|
109
116
|
this.startPoint = this.toStrokePoint(current);
|
110
117
|
this.builder = this.builderFactory(this.startPoint, this.editor.viewport);
|
118
|
+
this.currentDeviceType = current.device;
|
111
119
|
return true;
|
112
120
|
}
|
113
121
|
|
114
122
|
return false;
|
115
123
|
}
|
116
124
|
|
125
|
+
private eventCanCancelStroke(event: PointerEvt) {
|
126
|
+
// If there has been a delay since the last input event,
|
127
|
+
// it's always okay to cancel
|
128
|
+
const lastInputTime = this.lastPoint?.time ?? 0;
|
129
|
+
if (event.current.timeStamp - lastInputTime > 1000) {
|
130
|
+
return true;
|
131
|
+
}
|
132
|
+
|
133
|
+
const isPenStroke = this.currentDeviceType === PointerDevice.Pen;
|
134
|
+
const isTouchEvent = event.current.device === PointerDevice.Touch;
|
135
|
+
|
136
|
+
// Don't allow pen strokes to be cancelled by touch events.
|
137
|
+
if (isPenStroke && isTouchEvent) {
|
138
|
+
return false;
|
139
|
+
}
|
140
|
+
|
141
|
+
return true;
|
142
|
+
}
|
143
|
+
|
144
|
+
public override eventCanBeDeliveredToNonActiveTool(event: PointerEvt) {
|
145
|
+
return this.eventCanCancelStroke(event);
|
146
|
+
}
|
147
|
+
|
117
148
|
public override onPointerMove({ current }: PointerEvt): void {
|
118
149
|
if (!this.builder) return;
|
150
|
+
if (current.device !== this.currentDeviceType) return;
|
119
151
|
|
120
152
|
this.addPointToStroke(this.toStrokePoint(current));
|
121
153
|
}
|
122
154
|
|
123
|
-
public override onPointerUp({ current }: PointerEvt)
|
124
|
-
if (!this.builder)
|
125
|
-
|
155
|
+
public override onPointerUp({ current }: PointerEvt) {
|
156
|
+
if (!this.builder) return false;
|
157
|
+
if (current.device !== this.currentDeviceType) {
|
158
|
+
// this.builder still exists, so we're handling events from another
|
159
|
+
// device type.
|
160
|
+
return true;
|
126
161
|
}
|
127
162
|
|
128
163
|
// onPointerUp events can have zero pressure. Use the last pressure instead.
|
@@ -137,9 +172,12 @@ export default class Pen extends BaseTool {
|
|
137
172
|
if (current.isPrimary) {
|
138
173
|
this.finalizeStroke();
|
139
174
|
}
|
175
|
+
|
176
|
+
return false;
|
140
177
|
}
|
141
178
|
|
142
179
|
public override onGestureCancel() {
|
180
|
+
this.builder = null;
|
143
181
|
this.editor.clearWetInk();
|
144
182
|
}
|
145
183
|
|
@@ -115,7 +115,16 @@ export default class ToolController {
|
|
115
115
|
public dispatchInputEvent(event: InputEvt): boolean {
|
116
116
|
let handled = false;
|
117
117
|
if (event.kind === InputEvtType.PointerDownEvt) {
|
118
|
+
let canOnlySendToActiveTool = false;
|
119
|
+
if (this.activeTool && !this.activeTool.eventCanBeDeliveredToNonActiveTool(event)) {
|
120
|
+
canOnlySendToActiveTool = true;
|
121
|
+
}
|
122
|
+
|
118
123
|
for (const tool of this.tools) {
|
124
|
+
if (canOnlySendToActiveTool && tool !== this.activeTool) {
|
125
|
+
continue;
|
126
|
+
}
|
127
|
+
|
119
128
|
if (tool.isEnabled() && tool.onPointerDown(event)) {
|
120
129
|
if (this.activeTool !== tool) {
|
121
130
|
this.activeTool?.onGestureCancel();
|
@@ -127,8 +136,14 @@ export default class ToolController {
|
|
127
136
|
}
|
128
137
|
}
|
129
138
|
} else if (event.kind === InputEvtType.PointerUpEvt) {
|
130
|
-
this.activeTool?.onPointerUp(event);
|
131
|
-
|
139
|
+
const upResult = this.activeTool?.onPointerUp(event);
|
140
|
+
const continueHandlingEvents = upResult && event.allPointers.length > 1;
|
141
|
+
|
142
|
+
// Should the active tool continue handling events (without an additional pointer down?)
|
143
|
+
if (!continueHandlingEvents) {
|
144
|
+
// No -- Remove the current tool
|
145
|
+
this.activeTool = null;
|
146
|
+
}
|
132
147
|
handled = true;
|
133
148
|
} else if (event.kind === InputEvtType.PointerMoveEvt) {
|
134
149
|
if (this.activeTool !== null) {
|