js-draw 0.12.0 → 0.13.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/CHANGELOG.md +14 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.d.ts +12 -0
- package/dist/src/Color4.js +16 -0
- package/dist/src/Editor.d.ts +33 -18
- package/dist/src/Editor.js +22 -19
- package/dist/src/EditorImage.d.ts +12 -0
- package/dist/src/EditorImage.js +12 -0
- package/dist/src/Pointer.d.ts +1 -0
- package/dist/src/Pointer.js +8 -0
- package/dist/src/SVGLoader.d.ts +5 -0
- package/dist/src/SVGLoader.js +6 -1
- package/dist/src/Viewport.d.ts +30 -1
- package/dist/src/Viewport.js +39 -9
- package/dist/src/commands/invertCommand.js +1 -1
- package/dist/src/components/AbstractComponent.d.ts +19 -0
- package/dist/src/components/AbstractComponent.js +17 -2
- package/dist/src/lib.d.ts +6 -3
- package/dist/src/lib.js +4 -1
- package/dist/src/math/Mat33.d.ts +1 -1
- package/dist/src/math/Mat33.js +1 -1
- package/dist/src/rendering/Display.d.ts +9 -11
- package/dist/src/rendering/Display.js +12 -14
- package/dist/src/rendering/lib.d.ts +3 -0
- package/dist/src/rendering/lib.js +3 -0
- package/dist/src/rendering/renderers/DummyRenderer.js +2 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +4 -0
- package/dist/src/toolbar/IconProvider.d.ts +1 -1
- package/dist/src/toolbar/IconProvider.js +90 -29
- package/dist/src/toolbar/makeColorInput.js +8 -1
- package/dist/src/tools/PanZoom.d.ts +3 -1
- package/dist/src/tools/PanZoom.js +31 -6
- package/dist/src/tools/PasteHandler.d.ts +11 -4
- package/dist/src/tools/PasteHandler.js +12 -5
- package/dist/src/tools/Pen.d.ts +7 -2
- package/dist/src/tools/Pen.js +39 -6
- package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +3 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.js +6 -0
- package/dist/src/tools/SelectionTool/SelectionTool.d.ts +3 -1
- package/dist/src/tools/SelectionTool/SelectionTool.js +53 -15
- package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
- package/dist/src/tools/ToolSwitcherShortcut.js +9 -3
- package/dist/src/tools/UndoRedoShortcut.js +2 -4
- package/package.json +2 -2
- package/src/Color4.test.ts +11 -0
- package/src/Color4.ts +22 -0
- package/src/Editor.ts +36 -22
- package/src/EditorImage.ts +12 -0
- package/src/Pointer.ts +19 -0
- package/src/SVGLoader.ts +6 -1
- package/src/Viewport.ts +50 -11
- package/src/commands/invertCommand.ts +1 -1
- package/src/components/AbstractComponent.ts +33 -2
- package/src/lib.ts +6 -3
- package/src/math/Mat33.ts +1 -1
- package/src/rendering/Display.ts +12 -15
- package/src/rendering/RenderingStyle.ts +1 -1
- package/src/rendering/lib.ts +4 -0
- package/src/rendering/renderers/DummyRenderer.ts +2 -3
- package/src/rendering/renderers/SVGRenderer.ts +4 -0
- package/src/rendering/renderers/TextOnlyRenderer.ts +0 -1
- package/src/toolbar/HTMLToolbar.ts +1 -1
- package/src/toolbar/IconProvider.ts +98 -31
- package/src/toolbar/makeColorInput.ts +9 -1
- package/src/tools/PanZoom.ts +37 -7
- package/src/tools/PasteHandler.ts +12 -6
- package/src/tools/Pen.test.ts +44 -1
- package/src/tools/Pen.ts +53 -8
- package/src/tools/SelectionTool/SelectionHandle.ts +9 -0
- package/src/tools/SelectionTool/SelectionTool.ts +67 -15
- package/src/tools/ToolSwitcherShortcut.ts +10 -5
- package/src/tools/UndoRedoShortcut.ts +2 -5
- package/typedoc.json +2 -2
package/src/math/Mat33.ts
CHANGED
@@ -338,7 +338,7 @@ export default class Mat33 {
|
|
338
338
|
return result.rightMul(Mat33.translation(center.times(-1)));
|
339
339
|
}
|
340
340
|
|
341
|
-
/** @see {@link
|
341
|
+
/** @see {@link fromCSSMatrix} */
|
342
342
|
public toCSSMatrix(): string {
|
343
343
|
return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
|
344
344
|
}
|
package/src/rendering/Display.ts
CHANGED
@@ -1,18 +1,3 @@
|
|
1
|
-
/**
|
2
|
-
* Handles `HTMLCanvasElement`s (or other drawing surfaces if being used) used to display the editor's contents.
|
3
|
-
*
|
4
|
-
* @example
|
5
|
-
* ```
|
6
|
-
* const editor = new Editor(document.body);
|
7
|
-
* const w = editor.display.width;
|
8
|
-
* const h = editor.display.height;
|
9
|
-
* const center = Vec2.of(w / 2, h / 2);
|
10
|
-
* const colorAtCenter = editor.display.getColorAt(center);
|
11
|
-
* ```
|
12
|
-
*
|
13
|
-
* @packageDocumentation
|
14
|
-
*/
|
15
|
-
|
16
1
|
import AbstractRenderer from './renderers/AbstractRenderer';
|
17
2
|
import CanvasRenderer from './renderers/CanvasRenderer';
|
18
3
|
import { Editor } from '../Editor';
|
@@ -29,6 +14,18 @@ export enum RenderingMode {
|
|
29
14
|
// SVGRenderer is not supported by the main display
|
30
15
|
}
|
31
16
|
|
17
|
+
/**
|
18
|
+
* Handles `HTMLCanvasElement`s (or other drawing surfaces if being used) used to display the editor's contents.
|
19
|
+
*
|
20
|
+
* @example
|
21
|
+
* ```
|
22
|
+
* const editor = new Editor(document.body);
|
23
|
+
* const w = editor.display.width;
|
24
|
+
* const h = editor.display.height;
|
25
|
+
* const center = Vec2.of(w / 2, h / 2);
|
26
|
+
* const colorAtCenter = editor.display.getColorAt(center);
|
27
|
+
* ```
|
28
|
+
*/
|
32
29
|
export default class Display {
|
33
30
|
private dryInkRenderer: AbstractRenderer;
|
34
31
|
private wetInkRenderer: AbstractRenderer;
|
@@ -13,7 +13,7 @@ export default RenderingStyle;
|
|
13
13
|
export const stylesEqual = (a: RenderingStyle, b: RenderingStyle): boolean => {
|
14
14
|
const result = a === b || (a.fill.eq(b.fill)
|
15
15
|
&& (a.stroke == undefined) === (b.stroke == undefined)
|
16
|
-
|
16
|
+
&& (a.stroke?.color?.eq(b.stroke?.color) ?? true)
|
17
17
|
&& a.stroke?.width === b.stroke?.width);
|
18
18
|
|
19
19
|
// Map undefined/null -> false
|
@@ -1,5 +1,3 @@
|
|
1
|
-
// Renderer that outputs nothing. Useful for automated tests.
|
2
|
-
|
3
1
|
import { TextStyle } from '../../components/TextComponent';
|
4
2
|
import Mat33 from '../../math/Mat33';
|
5
3
|
import Rect2 from '../../math/Rect2';
|
@@ -9,6 +7,7 @@ import Viewport from '../../Viewport';
|
|
9
7
|
import RenderingStyle from '../RenderingStyle';
|
10
8
|
import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
|
11
9
|
|
10
|
+
// Renderer that outputs almost nothing. Useful for automated tests.
|
12
11
|
export default class DummyRenderer extends AbstractRenderer {
|
13
12
|
// Variables that track the state of what's been rendered
|
14
13
|
public clearedCount: number = 0;
|
@@ -28,7 +27,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
28
27
|
|
29
28
|
public displaySize(): Vec2 {
|
30
29
|
// Do we have a stored viewport size?
|
31
|
-
const viewportSize = this.getViewport().
|
30
|
+
const viewportSize = this.getViewport().getScreenRectSize();
|
32
31
|
|
33
32
|
// Don't use a 0x0 viewport — DummyRenderer is often used
|
34
33
|
// for tests that run without a display, so pretend we have a
|
@@ -38,6 +38,10 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
38
38
|
stroke-linecap: round;
|
39
39
|
stroke-linejoin: round;
|
40
40
|
}
|
41
|
+
|
42
|
+
text {
|
43
|
+
white-space: pre;
|
44
|
+
}
|
41
45
|
`.replace(/\s+/g, '');
|
42
46
|
styleSheet.setAttribute('id', renderedStylesheetId);
|
43
47
|
this.elem.appendChild(styleSheet);
|
@@ -9,7 +9,6 @@ import RenderingStyle from '../RenderingStyle';
|
|
9
9
|
import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
|
10
10
|
|
11
11
|
// Outputs a description of what was rendered.
|
12
|
-
|
13
12
|
export default class TextOnlyRenderer extends AbstractRenderer {
|
14
13
|
private descriptionBuilder: string[] = [];
|
15
14
|
private pathCount: number = 0;
|
@@ -43,7 +43,7 @@ export default class IconProvider {
|
|
43
43
|
return this.makeRedoIcon(true);
|
44
44
|
}
|
45
45
|
|
46
|
-
// @param mirror - reflect across the x-axis
|
46
|
+
// @param mirror - reflect across the x-axis. This parameter is internal.
|
47
47
|
// @returns a redo icon.
|
48
48
|
public makeRedoIcon(mirror: boolean = false): IconType {
|
49
49
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
@@ -383,52 +383,119 @@ export default class IconProvider {
|
|
383
383
|
return icon;
|
384
384
|
}
|
385
385
|
|
386
|
-
public makePenIcon(
|
386
|
+
public makePenIcon(strokeSize: number, color: string|Color4, rounded?: boolean): IconType {
|
387
387
|
if (color instanceof Color4) {
|
388
388
|
color = color.toHexString();
|
389
389
|
}
|
390
390
|
|
391
391
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
392
392
|
icon.setAttribute('viewBox', '0 0 100 100');
|
393
|
-
|
394
|
-
const halfThickness = tipThickness / 2;
|
395
|
-
|
396
|
-
// Draw a pen-like shape
|
397
|
-
const penTipLeft = 50 - halfThickness;
|
398
|
-
const penTipRight = 50 + halfThickness;
|
393
|
+
const tipThickness = strokeSize / 2;
|
399
394
|
|
400
|
-
|
401
|
-
|
395
|
+
const inkTipPath = `
|
396
|
+
M ${15 - tipThickness},${80 - tipThickness}
|
397
|
+
${15 - tipThickness},${80 + tipThickness}
|
398
|
+
30,83
|
399
|
+
15,65
|
400
|
+
Z
|
401
|
+
`;
|
402
|
+
const trailStartEndY = 80 + tipThickness;
|
403
|
+
const inkTrailPath = `
|
404
|
+
m ${15 - tipThickness * 1.1},${trailStartEndY}
|
405
|
+
c 35,10 55,15 60,30
|
406
|
+
l ${35 + tipThickness * 1.2},${-10 - tipThickness}
|
407
|
+
C 80.47,98.32 50.5,${90 + tipThickness} 20,${trailStartEndY} Z
|
408
|
+
`;
|
402
409
|
|
403
|
-
|
404
|
-
|
405
|
-
|
410
|
+
const colorBubblePath = `
|
411
|
+
M 72.45,35.67
|
412
|
+
A 10,15 41.8 0 1 55,40.2 10,15 41.8 0 1 57.55,22.3 10,15 41.8 0 1 75,17.8 10,15 41.8 0 1 72.5,35.67
|
413
|
+
Z
|
414
|
+
`;
|
415
|
+
|
416
|
+
let gripMainPath = 'M 85,-25 25,35 h 10 v 10 h 10 v 10 h 10 v 10 h 10 l -5,10 60,-60 z';
|
417
|
+
let gripShadow1Path = 'M 25,35 H 35 L 90,-15 85,-25 Z';
|
418
|
+
let gripShadow2Path = 'M 60,75 65,65 H 55 l 55,-55 10,5 z';
|
419
|
+
|
420
|
+
if (rounded) {
|
421
|
+
gripMainPath = 'M 85,-25 25,35 c 15,0 40,30 35,40 l 60,-60 z';
|
422
|
+
gripShadow1Path = 'm 25,35 c 3.92361,0.384473 7.644275,0.980572 10,3 l 55,-53 -5,-10 z';
|
423
|
+
gripShadow2Path = 'M 60,75 C 61,66 59,65 56,59 l 54,-54 10,10 z';
|
406
424
|
}
|
407
425
|
|
408
|
-
const
|
409
|
-
const backgroundStrokeTipPath = `M14,63 ${tipCenterBackgroundPath} L88,60 Z`;
|
426
|
+
const penTipPath = `M 25,35 ${10 - tipThickness / 4},${70 - tipThickness / 2} 20,75 25,85 60,75 70,55 45,25 Z`;
|
410
427
|
|
411
|
-
|
412
|
-
|
413
|
-
|
414
|
-
|
415
|
-
|
416
|
-
|
428
|
+
const pencilTipColor = Color4.fromHex('#f4d7d7');
|
429
|
+
const tipColor = pencilTipColor.mix(
|
430
|
+
Color4.fromString(color), tipThickness / 40 - 0.1
|
431
|
+
).toHexString();
|
432
|
+
|
433
|
+
const ink = `
|
434
|
+
<path
|
435
|
+
fill="${checkerboardPatternRef}"
|
436
|
+
d="${inkTipPath}"
|
437
|
+
/>
|
438
|
+
<path
|
439
|
+
fill="${checkerboardPatternRef}"
|
440
|
+
d="${inkTrailPath}"
|
441
|
+
/>
|
442
|
+
<path
|
443
|
+
fill="${color}"
|
444
|
+
d="${inkTipPath}"
|
445
|
+
/>
|
446
|
+
<path
|
447
|
+
fill="${color}"
|
448
|
+
d="${inkTrailPath}"
|
449
|
+
/>
|
450
|
+
`;
|
451
|
+
|
452
|
+
const penTip = `
|
453
|
+
<path
|
454
|
+
fill="${checkerboardPatternRef}"
|
455
|
+
d="${penTipPath}"
|
456
|
+
/>
|
457
|
+
<path
|
458
|
+
fill="${tipColor}"
|
459
|
+
stroke="${color}"
|
460
|
+
d="${penTipPath}"
|
461
|
+
/>
|
462
|
+
`;
|
463
|
+
|
464
|
+
const grip = `
|
417
465
|
<path
|
418
|
-
d='M10,10 L90,10 L90,60 L${50 + halfThickness},80 L${50 - halfThickness},80 L10,60 Z'
|
419
466
|
${iconColorStrokeFill}
|
467
|
+
d="${gripMainPath}"
|
468
|
+
/>
|
469
|
+
|
470
|
+
<!-- shadows -->
|
471
|
+
<path
|
472
|
+
fill="rgba(150, 150, 150, 0.3)"
|
473
|
+
d="${gripShadow1Path}"
|
420
474
|
/>
|
421
|
-
</g>
|
422
|
-
<g>
|
423
|
-
<!-- Checkerboard background for slightly transparent pens -->
|
424
|
-
<path d='${backgroundStrokeTipPath}' fill='${checkerboardPatternRef}'/>
|
425
|
-
|
426
|
-
<!-- Actual pen tip -->
|
427
475
|
<path
|
428
|
-
|
429
|
-
|
430
|
-
stroke='${color}'
|
476
|
+
fill="rgba(100, 100, 100, 0.2)"
|
477
|
+
d="${gripShadow2Path}"
|
431
478
|
/>
|
479
|
+
|
480
|
+
<!-- color bubble -->
|
481
|
+
<path
|
482
|
+
fill="${checkerboardPatternRef}"
|
483
|
+
d="${colorBubblePath}"
|
484
|
+
/>
|
485
|
+
<path
|
486
|
+
fill="${color}"
|
487
|
+
d="${colorBubblePath}"
|
488
|
+
/>
|
489
|
+
`;
|
490
|
+
|
491
|
+
icon.innerHTML = `
|
492
|
+
<defs>
|
493
|
+
${checkerboardPatternDef}
|
494
|
+
</defs>
|
495
|
+
<g>
|
496
|
+
${ink}
|
497
|
+
${penTip}
|
498
|
+
${grip}
|
432
499
|
</g>
|
433
500
|
`;
|
434
501
|
return icon;
|
@@ -19,7 +19,7 @@ export const makeColorInput = (
|
|
19
19
|
colorInputContainer.classList.add('color-input-container');
|
20
20
|
|
21
21
|
colorInputContainer.appendChild(colorInput);
|
22
|
-
addPipetteTool(editor, colorInputContainer, (color: Color4) => {
|
22
|
+
const pipetteController = addPipetteTool(editor, colorInputContainer, (color: Color4) => {
|
23
23
|
colorInput.value = color.toHexString();
|
24
24
|
onInputEnd();
|
25
25
|
|
@@ -58,6 +58,7 @@ export const makeColorInput = (
|
|
58
58
|
kind: EditorEventType.ColorPickerToggled,
|
59
59
|
open: true,
|
60
60
|
});
|
61
|
+
pipetteController.cancel();
|
61
62
|
});
|
62
63
|
colorInput.addEventListener('close', () => {
|
63
64
|
editor.notifier.dispatch(EditorEventType.ColorPickerToggled, {
|
@@ -132,6 +133,13 @@ const addPipetteTool = (editor: Editor, container: HTMLElement, onColorChange: O
|
|
132
133
|
};
|
133
134
|
|
134
135
|
container.appendChild(pipetteButton);
|
136
|
+
|
137
|
+
return {
|
138
|
+
// Cancel a pipette color selection if one is in progress.
|
139
|
+
cancel: () => {
|
140
|
+
endColorSelectMode();
|
141
|
+
},
|
142
|
+
};
|
135
143
|
};
|
136
144
|
|
137
145
|
export default makeColorInput;
|
package/src/tools/PanZoom.ts
CHANGED
@@ -90,11 +90,12 @@ class InertialScroller {
|
|
90
90
|
export default class PanZoom extends BaseTool {
|
91
91
|
private transform: ViewportTransform|null = null;
|
92
92
|
|
93
|
-
private lastAngle: number;
|
94
93
|
private lastDist: number;
|
95
94
|
private lastScreenCenter: Point2;
|
96
95
|
private lastTimestamp: number;
|
97
96
|
private lastPointerDownTimestamp: number = 0;
|
97
|
+
private initialTouchAngle: number = 0;
|
98
|
+
private initialViewportRotation: number = 0;
|
98
99
|
|
99
100
|
private inertialScroller: InertialScroller|null = null;
|
100
101
|
private velocity: Vec2|null = null;
|
@@ -132,9 +133,11 @@ export default class PanZoom extends BaseTool {
|
|
132
133
|
|
133
134
|
if (allAreTouch && pointers.length === 2 && this.mode & PanZoomMode.TwoFingerTouchGestures) {
|
134
135
|
const { screenCenter, angle, dist } = this.computePinchData(pointers[0], pointers[1]);
|
135
|
-
this.lastAngle = angle;
|
136
136
|
this.lastDist = dist;
|
137
137
|
this.lastScreenCenter = screenCenter;
|
138
|
+
this.initialTouchAngle = angle;
|
139
|
+
this.initialViewportRotation = this.editor.viewport.getRotationAngle();
|
140
|
+
|
138
141
|
handlingGesture = true;
|
139
142
|
} else if (pointers.length === 1 && (
|
140
143
|
(this.mode & PanZoomMode.OneFingerTouchGestures && allAreTouch)
|
@@ -188,24 +191,51 @@ export default class PanZoom extends BaseTool {
|
|
188
191
|
return delta;
|
189
192
|
}
|
190
193
|
|
194
|
+
// Snaps `angle` to common desired rotations. For example, if `touchAngle` corresponds
|
195
|
+
// to a viewport rotation of 90.1 degrees, this function returns a rotation delta that,
|
196
|
+
// when applied to the viewport, rotates the viewport to 90.0 degrees.
|
197
|
+
//
|
198
|
+
// Returns a snapped rotation delta that, when applied to the viewport, rotates the viewport,
|
199
|
+
// from its position on the last touchDown event, by `touchAngle - initialTouchAngle`.
|
200
|
+
private toSnappedRotationDelta(touchAngle: number) {
|
201
|
+
const deltaAngle = touchAngle - this.initialTouchAngle;
|
202
|
+
let fullRotation = deltaAngle + this.initialViewportRotation;
|
203
|
+
|
204
|
+
const snapToMultipleOf = Math.PI / 2;
|
205
|
+
const roundedFullRotation = Math.round(fullRotation / snapToMultipleOf) * snapToMultipleOf;
|
206
|
+
|
207
|
+
// The maximum angle for which we snap the given angle to a multiple of
|
208
|
+
// `snapToMultipleOf`.
|
209
|
+
const maxSnapAngle = 0.07;
|
210
|
+
|
211
|
+
// Snap the rotation
|
212
|
+
if (Math.abs(fullRotation - roundedFullRotation) < maxSnapAngle) {
|
213
|
+
fullRotation = roundedFullRotation;
|
214
|
+
}
|
215
|
+
|
216
|
+
return fullRotation - this.editor.viewport.getRotationAngle();
|
217
|
+
}
|
218
|
+
|
191
219
|
private handleTwoFingerMove(allPointers: Pointer[]) {
|
192
220
|
const { screenCenter, canvasCenter, angle, dist } = this.computePinchData(allPointers[0], allPointers[1]);
|
193
221
|
|
194
222
|
const delta = this.getCenterDelta(screenCenter);
|
195
|
-
let
|
223
|
+
let deltaRotation;
|
196
224
|
|
197
225
|
if (this.isRotationLocked()) {
|
198
|
-
|
226
|
+
deltaRotation = 0;
|
227
|
+
} else {
|
228
|
+
deltaRotation = this.toSnappedRotationDelta(angle);
|
199
229
|
}
|
200
230
|
|
231
|
+
|
201
232
|
this.updateVelocity(screenCenter);
|
202
233
|
|
203
234
|
const transformUpdate = Mat33.translation(delta)
|
204
235
|
.rightMul(Mat33.scaling2D(dist / this.lastDist, canvasCenter))
|
205
|
-
.rightMul(Mat33.zRotation(
|
236
|
+
.rightMul(Mat33.zRotation(deltaRotation, canvasCenter));
|
206
237
|
this.lastScreenCenter = screenCenter;
|
207
238
|
this.lastDist = dist;
|
208
|
-
this.lastAngle = angle;
|
209
239
|
this.transform = Viewport.transformBy(
|
210
240
|
this.transform!.transform.rightMul(transformUpdate)
|
211
241
|
);
|
@@ -338,7 +368,7 @@ export default class PanZoom extends BaseTool {
|
|
338
368
|
toCanvas.transformVec3(
|
339
369
|
Vec3.of(-delta.x, -delta.y, 0)
|
340
370
|
);
|
341
|
-
const pinchZoomScaleFactor = 1.
|
371
|
+
const pinchZoomScaleFactor = 1.03;
|
342
372
|
const transformUpdate = Mat33.scaling2D(
|
343
373
|
Math.max(0.25, Math.min(Math.pow(pinchZoomScaleFactor, -delta.z), 4)), canvasPos
|
344
374
|
).rightMul(
|
@@ -1,8 +1,3 @@
|
|
1
|
-
/**
|
2
|
-
* A tool that handles paste events.
|
3
|
-
* @packageDocumentation
|
4
|
-
*/
|
5
|
-
|
6
1
|
import Editor from '../Editor';
|
7
2
|
import { AbstractComponent, TextComponent } from '../components/lib';
|
8
3
|
import SVGLoader from '../SVGLoader';
|
@@ -14,12 +9,23 @@ import Color4 from '../Color4';
|
|
14
9
|
import { TextStyle } from '../components/TextComponent';
|
15
10
|
import ImageComponent from '../components/ImageComponent';
|
16
11
|
|
17
|
-
|
12
|
+
/**
|
13
|
+
* A tool that handles paste events (e.g. as triggered by ctrl+V).
|
14
|
+
*
|
15
|
+
* @example
|
16
|
+
* While `ToolController` has a `PasteHandler` in its default list of tools,
|
17
|
+
* if a non-default set is being used, `PasteHandler` can be added as follows:
|
18
|
+
* ```ts
|
19
|
+
* const toolController = editor.toolController;
|
20
|
+
* toolController.addTool(new PasteHandler(editor));
|
21
|
+
* ```
|
22
|
+
*/
|
18
23
|
export default class PasteHandler extends BaseTool {
|
19
24
|
public constructor(private editor: Editor) {
|
20
25
|
super(editor.notifier, editor.localization.pasteHandler);
|
21
26
|
}
|
22
27
|
|
28
|
+
// @internal
|
23
29
|
public onPaste(event: PasteEvent): boolean {
|
24
30
|
const mime = event.mime.toLowerCase();
|
25
31
|
|
package/src/tools/Pen.test.ts
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
|
2
|
-
import
|
2
|
+
import PenTool from './Pen';
|
3
3
|
import { Vec2 } from '../math/Vec2';
|
4
4
|
import createEditor from '../testing/createEditor';
|
5
5
|
import { InputEvtType } from '../types';
|
6
|
+
import Rect2 from '../math/Rect2';
|
7
|
+
import StrokeComponent from '../components/Stroke';
|
8
|
+
import Mat33 from '../math/Mat33';
|
9
|
+
import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
|
6
10
|
|
7
11
|
describe('Pen', () => {
|
8
12
|
it('should draw horizontal lines', () => {
|
@@ -147,4 +151,43 @@ describe('Pen', () => {
|
|
147
151
|
expect(elems[0].getBBox().topLeft).objEq(Vec2.of(420, 24), 32); // ± 32
|
148
152
|
expect(elems[0].getBBox().bottomRight).objEq(Vec2.of(420, 340), 25); // ± 25
|
149
153
|
});
|
154
|
+
|
155
|
+
it('ctrl+z should finalize then undo the current stroke', async () => {
|
156
|
+
const editor = createEditor();
|
157
|
+
|
158
|
+
expect(editor.history.undoStackSize).toBe(0);
|
159
|
+
|
160
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(10, 10));
|
161
|
+
jest.advanceTimersByTime(100);
|
162
|
+
editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(20, 10));
|
163
|
+
|
164
|
+
const ctrlKeyDown = true;
|
165
|
+
editor.sendKeyboardEvent(InputEvtType.KeyPressEvent, 'z', ctrlKeyDown);
|
166
|
+
|
167
|
+
// Stroke should have been undone
|
168
|
+
expect(editor.history.redoStackSize).toBe(1);
|
169
|
+
|
170
|
+
// Lifting the pointer up shouldn't clear the redo stack.
|
171
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(420, 340));
|
172
|
+
expect(editor.history.redoStackSize).toBe(1);
|
173
|
+
});
|
174
|
+
|
175
|
+
it('holding ctrl should snap the stroke to grid', () => {
|
176
|
+
const editor = createEditor();
|
177
|
+
editor.viewport.resetTransform(Mat33.identity);
|
178
|
+
|
179
|
+
const penTool = editor.toolController.getMatchingTools(PenTool)[0];
|
180
|
+
penTool.setStrokeFactory(makeFreehandLineBuilder);
|
181
|
+
|
182
|
+
editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0.1, 0.1));
|
183
|
+
jest.advanceTimersByTime(100);
|
184
|
+
editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(10.1, 10.1));
|
185
|
+
editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(10.1, 10.1));
|
186
|
+
|
187
|
+
const allElems = editor.image.getAllElements();
|
188
|
+
expect(allElems).toHaveLength(1);
|
189
|
+
|
190
|
+
const firstStroke = allElems[0] as StrokeComponent;
|
191
|
+
expect(firstStroke.getPath().bbox).objEq(new Rect2(0, 0, 10, 10));
|
192
|
+
});
|
150
193
|
});
|
package/src/tools/Pen.ts
CHANGED
@@ -3,7 +3,7 @@ import Editor from '../Editor';
|
|
3
3
|
import EditorImage from '../EditorImage';
|
4
4
|
import Pointer, { PointerDevice } from '../Pointer';
|
5
5
|
import { makeFreehandLineBuilder } from '../components/builders/FreehandLineBuilder';
|
6
|
-
import { EditorEventType, KeyPressEvent, PointerEvt, StrokeDataPoint } from '../types';
|
6
|
+
import { EditorEventType, KeyPressEvent, KeyUpEvent, PointerEvt, StrokeDataPoint } from '../types';
|
7
7
|
import BaseTool from './BaseTool';
|
8
8
|
import { ComponentBuilder, ComponentBuilderFactory } from '../components/builders/types';
|
9
9
|
|
@@ -15,6 +15,7 @@ export interface PenStyle {
|
|
15
15
|
export default class Pen extends BaseTool {
|
16
16
|
protected builder: ComponentBuilder|null = null;
|
17
17
|
private lastPoint: StrokeDataPoint|null = null;
|
18
|
+
private ctrlKeyPressed: boolean = false;
|
18
19
|
|
19
20
|
public constructor(
|
20
21
|
private editor: Editor,
|
@@ -31,6 +32,10 @@ export default class Pen extends BaseTool {
|
|
31
32
|
|
32
33
|
// Converts a `pointer` to a `StrokeDataPoint`.
|
33
34
|
protected toStrokePoint(pointer: Pointer): StrokeDataPoint {
|
35
|
+
if (this.isSnappingToGrid()) {
|
36
|
+
pointer = pointer.snappedToGrid(this.editor.viewport);
|
37
|
+
}
|
38
|
+
|
34
39
|
const minPressure = 0.3;
|
35
40
|
let pressure = Math.max(pointer.pressure ?? 1.0, minPressure);
|
36
41
|
|
@@ -42,8 +47,10 @@ export default class Pen extends BaseTool {
|
|
42
47
|
console.assert(isFinite(pointer.screenPos.length()), 'Non-finite screen position!');
|
43
48
|
console.assert(isFinite(pointer.timeStamp), 'Non-finite timeStamp on pointer!');
|
44
49
|
|
50
|
+
const pos = pointer.canvasPos;
|
51
|
+
|
45
52
|
return {
|
46
|
-
pos
|
53
|
+
pos,
|
47
54
|
width: pressure * this.getPressureMultiplier(),
|
48
55
|
color: this.style.color,
|
49
56
|
time: pointer.timeStamp,
|
@@ -86,6 +93,8 @@ export default class Pen extends BaseTool {
|
|
86
93
|
}
|
87
94
|
|
88
95
|
public onPointerMove({ current }: PointerEvt): void {
|
96
|
+
if (!this.builder) return;
|
97
|
+
|
89
98
|
this.addPointToStroke(this.toStrokePoint(current));
|
90
99
|
}
|
91
100
|
|
@@ -102,7 +111,18 @@ export default class Pen extends BaseTool {
|
|
102
111
|
};
|
103
112
|
|
104
113
|
this.addPointToStroke(strokePoint);
|
105
|
-
|
114
|
+
|
115
|
+
if (current.isPrimary) {
|
116
|
+
this.finalizeStroke();
|
117
|
+
}
|
118
|
+
}
|
119
|
+
|
120
|
+
public onGestureCancel() {
|
121
|
+
this.editor.clearWetInk();
|
122
|
+
}
|
123
|
+
|
124
|
+
private finalizeStroke() {
|
125
|
+
if (this.builder) {
|
106
126
|
const stroke = this.builder.build();
|
107
127
|
this.previewStroke();
|
108
128
|
|
@@ -118,10 +138,6 @@ export default class Pen extends BaseTool {
|
|
118
138
|
this.editor.clearWetInk();
|
119
139
|
}
|
120
140
|
|
121
|
-
public onGestureCancel() {
|
122
|
-
this.editor.clearWetInk();
|
123
|
-
}
|
124
|
-
|
125
141
|
private noteUpdated() {
|
126
142
|
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
127
143
|
kind: EditorEventType.ToolUpdated,
|
@@ -160,7 +176,15 @@ export default class Pen extends BaseTool {
|
|
160
176
|
public getColor() { return this.style.color; }
|
161
177
|
public getStrokeFactory() { return this.builderFactory; }
|
162
178
|
|
163
|
-
public
|
179
|
+
public setEnabled(enabled: boolean): void {
|
180
|
+
super.setEnabled(enabled);
|
181
|
+
|
182
|
+
this.ctrlKeyPressed = false;
|
183
|
+
}
|
184
|
+
|
185
|
+
private isSnappingToGrid() { return this.ctrlKeyPressed; }
|
186
|
+
|
187
|
+
public onKeyPress({ key, ctrlKey }: KeyPressEvent): boolean {
|
164
188
|
key = key.toLowerCase();
|
165
189
|
|
166
190
|
let newThickness: number|undefined;
|
@@ -176,6 +200,27 @@ export default class Pen extends BaseTool {
|
|
176
200
|
return true;
|
177
201
|
}
|
178
202
|
|
203
|
+
if (key === 'control') {
|
204
|
+
this.ctrlKeyPressed = true;
|
205
|
+
return true;
|
206
|
+
}
|
207
|
+
|
208
|
+
// Ctrl+Z: End the stroke so that it can be undone/redone.
|
209
|
+
if (key === 'z' && ctrlKey && this.builder) {
|
210
|
+
this.finalizeStroke();
|
211
|
+
}
|
212
|
+
|
213
|
+
return false;
|
214
|
+
}
|
215
|
+
|
216
|
+
public onKeyUp({ key }: KeyUpEvent): boolean {
|
217
|
+
key = key.toLowerCase();
|
218
|
+
|
219
|
+
if (key === 'control') {
|
220
|
+
this.ctrlKeyPressed = false;
|
221
|
+
return true;
|
222
|
+
}
|
223
|
+
|
179
224
|
return false;
|
180
225
|
}
|
181
226
|
}
|
@@ -18,6 +18,7 @@ export type DragEndCallback = ()=> void;
|
|
18
18
|
|
19
19
|
export default class SelectionHandle {
|
20
20
|
private element: HTMLElement;
|
21
|
+
private snapToGrid: boolean;
|
21
22
|
|
22
23
|
// Bounding box in screen coordinates.
|
23
24
|
|
@@ -96,4 +97,12 @@ export default class SelectionHandle {
|
|
96
97
|
}
|
97
98
|
this.onDragEnd();
|
98
99
|
}
|
100
|
+
|
101
|
+
public setSnapToGrid(snap: boolean) {
|
102
|
+
this.snapToGrid = snap;
|
103
|
+
}
|
104
|
+
|
105
|
+
public isSnappingToGrid() {
|
106
|
+
return this.snapToGrid;
|
107
|
+
}
|
99
108
|
}
|