js-draw 0.4.0 → 0.4.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.
Files changed (45) hide show
  1. package/.github/pull_request_template.md +15 -0
  2. package/.github/workflows/firebase-hosting-merge.yml +7 -0
  3. package/.github/workflows/firebase-hosting-pull-request.yml +10 -0
  4. package/.github/workflows/github-pages.yml +2 -0
  5. package/CHANGELOG.md +7 -0
  6. package/dist/bundle.js +1 -1
  7. package/dist/src/Editor.js +3 -1
  8. package/dist/src/components/AbstractComponent.js +1 -0
  9. package/dist/src/components/Stroke.js +15 -9
  10. package/dist/src/components/Text.d.ts +1 -1
  11. package/dist/src/components/Text.js +1 -1
  12. package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -0
  13. package/dist/src/components/builders/FreehandLineBuilder.js +34 -36
  14. package/dist/src/math/Vec3.d.ts +1 -1
  15. package/dist/src/math/Vec3.js +1 -1
  16. package/dist/src/testing/beforeEachFile.d.ts +1 -0
  17. package/dist/src/testing/beforeEachFile.js +3 -0
  18. package/dist/src/testing/createEditor.d.ts +1 -0
  19. package/dist/src/testing/createEditor.js +7 -1
  20. package/dist/src/testing/loadExpectExtensions.d.ts +0 -15
  21. package/dist/src/tools/SelectionTool/SelectionTool.js +8 -0
  22. package/jest.config.js +5 -0
  23. package/package.json +15 -14
  24. package/src/Editor.ts +2 -0
  25. package/src/components/AbstractComponent.ts +2 -0
  26. package/src/components/Stroke.test.ts +0 -3
  27. package/src/components/Stroke.ts +14 -7
  28. package/src/components/Text.test.ts +0 -3
  29. package/src/components/Text.ts +2 -2
  30. package/src/components/builders/FreehandLineBuilder.ts +37 -43
  31. package/src/language/assertions.ts +2 -2
  32. package/src/math/LineSegment2.test.ts +8 -10
  33. package/src/math/Mat33.test.ts +0 -2
  34. package/src/math/Rect2.test.ts +0 -3
  35. package/src/math/Vec2.test.ts +0 -3
  36. package/src/math/Vec3.test.ts +0 -3
  37. package/src/math/Vec3.ts +1 -1
  38. package/src/testing/beforeEachFile.ts +3 -0
  39. package/src/testing/createEditor.ts +8 -1
  40. package/src/testing/global.d.ts +17 -0
  41. package/src/testing/loadExpectExtensions.ts +0 -15
  42. package/src/toolbar/toolbar.css +3 -2
  43. package/src/tools/Pen.test.ts +150 -0
  44. package/src/tools/SelectionTool/SelectionTool.ts +9 -0
  45. package/tsconfig.json +3 -1
@@ -173,7 +173,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
173
173
  }
174
174
 
175
175
  public build(): Stroke {
176
- if (this.lastPoint) {
176
+ if (this.lastPoint && (this.lowerSegments.length === 0 || this.approxCurrentCurveLength() > this.curveStartWidth * 2)) {
177
177
  this.finalizeCurrentCurve();
178
178
  }
179
179
  return this.previewStroke()!;
@@ -189,6 +189,19 @@ export default class FreehandLineBuilder implements ComponentBuilder {
189
189
  return Viewport.roundPoint(point, minFit);
190
190
  }
191
191
 
192
+ // Returns the distance between the start, control, and end points of the curve.
193
+ private approxCurrentCurveLength() {
194
+ if (!this.currentCurve) {
195
+ return 0;
196
+ }
197
+ const startPt = Vec2.ofXY(this.currentCurve.points[0]);
198
+ const controlPt = Vec2.ofXY(this.currentCurve.points[1]);
199
+ const endPt = Vec2.ofXY(this.currentCurve.points[2]);
200
+ const toControlDist = startPt.minus(controlPt).length();
201
+ const toEndDist = endPt.minus(controlPt).length();
202
+ return toControlDist + toEndDist;
203
+ }
204
+
192
205
  private finalizeCurrentCurve() {
193
206
  // Case where no points have been added
194
207
  if (!this.currentCurve) {
@@ -285,10 +298,8 @@ export default class FreehandLineBuilder implements ComponentBuilder {
285
298
  startVec = startVec.times(this.curveStartWidth / 2);
286
299
  endVec = endVec.times(this.curveEndWidth / 2);
287
300
 
288
- if (isNaN(startVec.magnitude())) {
289
- // TODO: This can happen when events are too close together. Find out why and
290
- // fix.
291
- console.error('startVec is NaN', startVec, endVec, this.currentCurve);
301
+ if (!isFinite(startVec.magnitude())) {
302
+ console.error('Warning: startVec is NaN or ∞', startVec, endVec, this.currentCurve);
292
303
  startVec = endVec;
293
304
  }
294
305
 
@@ -307,39 +318,22 @@ export default class FreehandLineBuilder implements ComponentBuilder {
307
318
  }
308
319
 
309
320
  const halfVecT = projectionT;
310
- let halfVec = Vec2.ofXY(this.currentCurve.normal(halfVecT))
321
+ const halfVec = Vec2.ofXY(this.currentCurve.normal(halfVecT))
311
322
  .normalized().times(
312
323
  this.curveStartWidth / 2 * halfVecT
313
324
  + this.curveEndWidth / 2 * (1 - halfVecT)
314
325
  );
315
326
 
316
- // Computes a boundary curve. [direction] should be either +1 or -1 (determines the side
317
- // of the center curve to place the boundary).
318
- const computeBoundaryCurve = (direction: number, halfVec: Vec2) => {
319
- return new Bezier(
320
- startPt.plus(startVec.times(direction)),
321
- controlPoint.plus(halfVec.times(direction)),
322
- endPt.plus(endVec.times(direction)),
323
- );
324
- };
325
-
326
- const boundariesIntersect = () => {
327
- const upperBoundary = computeBoundaryCurve(1, halfVec);
328
- const lowerBoundary = computeBoundaryCurve(-1, halfVec);
329
- return upperBoundary.intersects(lowerBoundary).length > 0;
330
- };
331
-
332
- // If the boundaries have intersections, increasing the half vector's length could fix this.
333
- if (boundariesIntersect()) {
334
- halfVec = halfVec.times(1.1);
335
- }
336
-
337
327
  // Each starts at startPt ± startVec
328
+ const lowerCurveControlPoint = this.roundPoint(controlPoint.plus(halfVec));
329
+ const lowerCurveEndPoint = this.roundPoint(endPt.plus(endVec));
330
+ const upperCurveControlPoint = this.roundPoint(controlPoint.minus(halfVec));
331
+ const upperCurveStartPoint = this.roundPoint(endPt.minus(endVec));
338
332
 
339
333
  const lowerCurve: QuadraticBezierPathCommand = {
340
334
  kind: PathCommandType.QuadraticBezierTo,
341
- controlPoint: this.roundPoint(controlPoint.plus(halfVec)),
342
- endPoint: this.roundPoint(endPt.plus(endVec)),
335
+ controlPoint: lowerCurveControlPoint,
336
+ endPoint: lowerCurveEndPoint,
343
337
  };
344
338
 
345
339
  // From the end of the upperCurve to the start of the lowerCurve:
@@ -351,12 +345,12 @@ export default class FreehandLineBuilder implements ComponentBuilder {
351
345
  // From the end of lowerCurve to the start of upperCurve:
352
346
  const lowerToUpperConnector: LinePathCommand = {
353
347
  kind: PathCommandType.LineTo,
354
- point: this.roundPoint(endPt.minus(endVec))
348
+ point: upperCurveStartPoint,
355
349
  };
356
350
 
357
351
  const upperCurve: QuadraticBezierPathCommand = {
358
352
  kind: PathCommandType.QuadraticBezierTo,
359
- controlPoint: this.roundPoint(controlPoint.minus(halfVec)),
353
+ controlPoint: upperCurveControlPoint,
360
354
  endPoint: this.roundPoint(startPt.minus(startVec)),
361
355
  };
362
356
 
@@ -374,7 +368,6 @@ export default class FreehandLineBuilder implements ComponentBuilder {
374
368
  const fuzzEq = 1e-10;
375
369
  const deltaTime = newPoint.time - this.lastPoint.time;
376
370
  if (newPoint.pos.eq(this.lastPoint.pos, fuzzEq) || deltaTime === 0) {
377
- console.warn('Discarding identical point');
378
371
  return;
379
372
  } else if (isNaN(newPoint.pos.magnitude())) {
380
373
  console.warn('Discarding NaN point.', newPoint);
@@ -433,23 +426,23 @@ export default class FreehandLineBuilder implements ComponentBuilder {
433
426
  let exitingVec = this.computeExitingVec();
434
427
 
435
428
  // Find the intersection between the entering vector and the exiting vector
436
- const maxRelativeLength = 2;
429
+ const maxRelativeLength = 3;
437
430
  const segmentStart = this.buffer[0];
438
431
  const segmentEnd = newPoint.pos;
439
432
  const startEndDist = segmentEnd.minus(segmentStart).magnitude();
440
433
  const maxControlPointDist = maxRelativeLength * startEndDist;
441
434
 
442
435
  // Exit in cases where we would divide by zero
443
- if (maxControlPointDist === 0 || exitingVec.magnitude() === 0 || isNaN(exitingVec.magnitude())) {
436
+ if (maxControlPointDist === 0 || exitingVec.magnitude() === 0 || !isFinite(exitingVec.magnitude())) {
444
437
  return;
445
438
  }
446
439
 
447
- console.assert(!isNaN(enteringVec.magnitude()));
440
+ console.assert(isFinite(enteringVec.magnitude()), 'Pre-normalized enteringVec has NaN or ∞ magnitude!');
448
441
 
449
442
  enteringVec = enteringVec.normalized();
450
443
  exitingVec = exitingVec.normalized();
451
444
 
452
- console.assert(!isNaN(enteringVec.magnitude()));
445
+ console.assert(isFinite(enteringVec.magnitude()), 'Normalized enteringVec has NaN or ∞ magnitude!');
453
446
 
454
447
  const lineFromStart = new LineSegment2(
455
448
  segmentStart,
@@ -462,18 +455,20 @@ export default class FreehandLineBuilder implements ComponentBuilder {
462
455
  const intersection = lineFromEnd.intersection(lineFromStart);
463
456
 
464
457
  // Position the control point at this intersection
465
- let controlPoint: Point2;
458
+ let controlPoint: Point2|null = null;
466
459
  if (intersection) {
467
460
  controlPoint = intersection.point;
468
- } else {
461
+ }
462
+
463
+ // No intersection or the intersection is one of the end points?
464
+ if (!controlPoint || segmentStart.eq(controlPoint) || segmentEnd.eq(controlPoint)) {
469
465
  // Position the control point closer to the first -- the connecting
470
466
  // segment will be roughly a line.
471
467
  controlPoint = segmentStart.plus(enteringVec.times(startEndDist / 4));
472
468
  }
473
469
 
474
- if (isNaN(controlPoint.magnitude()) || isNaN(segmentStart.magnitude())) {
475
- console.error('controlPoint is NaN', intersection, 'Start:', segmentStart, 'End:', segmentEnd, 'in:', enteringVec, 'out:', exitingVec);
476
- }
470
+ console.assert(!segmentStart.eq(controlPoint, 1e-11), 'Start and control points are equal!');
471
+ console.assert(!controlPoint.eq(segmentEnd, 1e-11), 'Control and end points are equal!');
477
472
 
478
473
  const prevCurve = this.currentCurve;
479
474
  this.currentCurve = new Bezier(segmentStart.xy, controlPoint.xy, segmentEnd.xy);
@@ -502,8 +497,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
502
497
  return true;
503
498
  };
504
499
 
505
- const approxCurveLen = controlPoint.minus(segmentStart).magnitude() + segmentEnd.minus(controlPoint).magnitude();
506
- if (this.buffer.length > 3 && approxCurveLen > this.curveEndWidth / 3) {
500
+ if (this.buffer.length > 3 && this.approxCurrentCurveLength() > this.curveStartWidth) {
507
501
  if (!curveMatchesPoints(this.currentCurve)) {
508
502
  // Use a curve that better fits the points
509
503
  this.currentCurve = prevCurve;
@@ -2,5 +2,5 @@
2
2
  // Compile-time assertion that a branch of code is unreachable.
3
3
  // See https://stackoverflow.com/a/39419171/17055750
4
4
  export const assertUnreachable = (key: never): never => {
5
- throw new Error(`Should be unreachable. Key: ${key}.`);
6
- };
5
+ throw new Error(`Should be unreachable. Key: ${key}.`);
6
+ };
@@ -1,9 +1,7 @@
1
1
  import LineSegment2 from './LineSegment2';
2
- import { loadExpectExtensions } from '../testing/loadExpectExtensions';
3
2
  import { Vec2 } from './Vec2';
4
3
  import Mat33 from './Mat33';
5
4
 
6
- loadExpectExtensions();
7
5
 
8
6
  describe('Line2', () => {
9
7
  it('x and y axes should intersect at (0, 0)', () => {
@@ -14,13 +12,13 @@ describe('Line2', () => {
14
12
  });
15
13
 
16
14
  it('y = -2x + 2 and y = 2x - 2 should intersect at (1,0)', () => {
17
- // y = -2x + 2
15
+ // y = -4x + 2
18
16
  const line1 = new LineSegment2(Vec2.of(0, 2), Vec2.of(1, -2));
19
- // y = 2x - 2
17
+ // y = 4x - 2
20
18
  const line2 = new LineSegment2(Vec2.of(0, -2), Vec2.of(1, 2));
21
19
 
22
- expect(line1.intersection(line2)?.point).objEq(Vec2.of(1, 0));
23
- expect(line2.intersection(line1)?.point).objEq(Vec2.of(1, 0));
20
+ expect(line1.intersection(line2)?.point).objEq(Vec2.of(0.5, 0));
21
+ expect(line2.intersection(line1)?.point).objEq(Vec2.of(0.5, 0));
24
22
  });
25
23
 
26
24
  it('line from (10, 10) to (-100, 10) should intersect with the y-axis at t = 10', () => {
@@ -81,14 +79,14 @@ describe('Line2', () => {
81
79
  expect(line.closestPointTo(Vec2.zero)).objEq(Vec2.of(1, 0));
82
80
  });
83
81
 
84
- it('Closest point from (-1,2) to segment((1,1) -> (2,4)) should be (1,1)', () => {
82
+ it('Closest point from (-1,-2) to segment((1,1) -> (2,4)) should be (1,1)', () => {
85
83
  const line = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 4));
86
- expect(line.closestPointTo(Vec2.of(-1, 2))).objEq(Vec2.of(1, 1));
84
+ expect(line.closestPointTo(Vec2.of(-1, -2))).objEq(Vec2.of(1, 1));
87
85
  });
88
86
 
89
- it('Closest point from (5,2) to segment((1,1) -> (2,4)) should be (2,4)', () => {
87
+ it('Closest point from (5,8) to segment((1,1) -> (2,4)) should be (2,4)', () => {
90
88
  const line = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 4));
91
- expect(line.closestPointTo(Vec2.of(5, 2))).objEq(Vec2.of(2, 4));
89
+ expect(line.closestPointTo(Vec2.of(5, 8))).objEq(Vec2.of(2, 4));
92
90
  });
93
91
 
94
92
  it('Should translate when translated by a translation matrix', () => {
@@ -1,9 +1,7 @@
1
1
  import Mat33 from './Mat33';
2
2
  import { Vec2 } from './Vec2';
3
- import { loadExpectExtensions } from '../testing/loadExpectExtensions';
4
3
  import Vec3 from './Vec3';
5
4
 
6
- loadExpectExtensions();
7
5
 
8
6
  describe('Mat33 tests', () => {
9
7
  it('equality', () => {
@@ -1,11 +1,8 @@
1
1
 
2
2
  import Rect2 from './Rect2';
3
3
  import { Vec2 } from './Vec2';
4
- import loadExpectExtensions from '../testing/loadExpectExtensions';
5
4
  import Mat33 from './Mat33';
6
5
 
7
- loadExpectExtensions();
8
-
9
6
  describe('Rect2', () => {
10
7
  it('width, height should always be positive', () => {
11
8
  expect(new Rect2(-1, -2, -3, 4)).objEq(new Rect2(-4, -2, 3, 4));
@@ -1,8 +1,5 @@
1
1
  import { Vec2 } from './Vec2';
2
2
  import Vec3 from './Vec3';
3
- import { loadExpectExtensions } from '../testing/loadExpectExtensions';
4
-
5
- loadExpectExtensions();
6
3
 
7
4
  describe('Vec2', () => {
8
5
  it('Magnitude', () => {
@@ -1,9 +1,6 @@
1
1
 
2
- import { loadExpectExtensions } from '../testing/loadExpectExtensions';
3
2
  import Vec3 from './Vec3';
4
3
 
5
- loadExpectExtensions();
6
-
7
4
  describe('Vec3', () => {
8
5
  it('.xy should contain the x and y components', () => {
9
6
  const vec = Vec3.of(1, 2, 3);
package/src/math/Vec3.ts CHANGED
@@ -196,7 +196,7 @@ export default class Vec3 {
196
196
  * Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 2.99); // → false
197
197
  * ```
198
198
  */
199
- public eq(other: Vec3, fuzz: number): boolean {
199
+ public eq(other: Vec3, fuzz: number = 1e-10): boolean {
200
200
  for (let i = 0; i < 3; i++) {
201
201
  if (Math.abs(other.at(i) - this.at(i)) > fuzz) {
202
202
  return false;
@@ -0,0 +1,3 @@
1
+ import loadExpectExtensions from './loadExpectExtensions';
2
+ loadExpectExtensions();
3
+ jest.useFakeTimers();
@@ -1,4 +1,11 @@
1
1
  import { RenderingMode } from '../rendering/Display';
2
2
  import Editor from '../Editor';
3
3
 
4
- export default () => new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });
4
+ /** Creates an editor. Should only be used in test files. */
5
+ export default () => {
6
+ if (jest === undefined) {
7
+ throw new Error('Files in the testing/ folder should only be used in tests!');
8
+ }
9
+
10
+ return new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });
11
+ };
@@ -0,0 +1,17 @@
1
+
2
+
3
+ // Type declarations for custom matchers
4
+ interface CustomMatchers<R = unknown> {
5
+ objEq(expected: {
6
+ eq: (other: any, ...args: any)=> boolean;
7
+ }, ...opts: any): R;
8
+ }
9
+
10
+ declare namespace jest {
11
+ interface Expect extends CustomMatchers {}
12
+ interface Matchers<R> extends CustomMatchers<R> {}
13
+ interface AsyncAsymmetricMatchers extends CustomMatchers {}
14
+ }
15
+
16
+ declare interface JestMatchers<T> extends CustomMatchers<T> {
17
+ }
@@ -22,19 +22,4 @@ export const loadExpectExtensions = () => {
22
22
  });
23
23
  };
24
24
 
25
- // Type declarations for custom matchers
26
- export interface CustomMatchers<R = unknown> {
27
- objEq(expected: {
28
- eq: (other: any, ...args: any)=> boolean;
29
- }, ...opts: any): R;
30
- }
31
-
32
- declare global {
33
- export namespace jest {
34
- interface Expect extends CustomMatchers {}
35
- interface Matchers<R> extends CustomMatchers<R> {}
36
- interface AsyncAsymmetricMatchers extends CustomMatchers {}
37
- }
38
- }
39
-
40
25
  export default loadExpectExtensions;
@@ -34,7 +34,8 @@
34
34
  }
35
35
 
36
36
  .toolbar-button.disabled {
37
- filter: opacity(0.8) saturate(0.1);
37
+ filter: opacity(0.5) sepia(0.2);
38
+ cursor: unset;
38
39
  }
39
40
 
40
41
  .toolbar-button, .toolbar-root button {
@@ -75,7 +76,7 @@
75
76
  width: 6em;
76
77
  }
77
78
 
78
- .toolbar-button:hover, .toolbar-root button:not(:disabled):hover {
79
+ .toolbar-button:not(.disabled):hover, .toolbar-root button:not(:disabled):hover {
79
80
  box-shadow: 0px 2px 4px var(--primary-shadow-color);
80
81
  }
81
82
 
@@ -0,0 +1,150 @@
1
+
2
+ import { Rect2 } from '../lib';
3
+ import { Vec2 } from '../math/Vec2';
4
+ import createEditor from '../testing/createEditor';
5
+ import { InputEvtType } from '../types';
6
+
7
+ describe('Pen', () => {
8
+ it('should draw horizontal lines', () => {
9
+ const editor = createEditor();
10
+ editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
11
+ for (let i = 0; i < 10; i++) {
12
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(i, 0));
13
+ jest.advanceTimersByTime(200);
14
+ }
15
+ editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(200, 0));
16
+
17
+ const elems = editor.image.getElementsIntersectingRegion(new Rect2(0, 10, 10, -10));
18
+ expect(elems).toHaveLength(1);
19
+
20
+ // Account for stroke width
21
+ const tolerableError = 8;
22
+ expect(elems[0].getBBox().topLeft).objEq(Vec2.of(0, 0), tolerableError);
23
+ expect(elems[0].getBBox().bottomRight).objEq(Vec2.of(200, 0), tolerableError);
24
+ });
25
+
26
+ it('should draw vertical line', () => {
27
+ const editor = createEditor();
28
+ editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(0, 0));
29
+ for (let i = 0; i < 10; i++) {
30
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(0, i * 20));
31
+ jest.advanceTimersByTime(200);
32
+ }
33
+ editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(0, 150));
34
+
35
+ const elems = editor.image.getElementsIntersectingRegion(Rect2.unitSquare);
36
+ expect(elems).toHaveLength(1);
37
+
38
+ expect(elems[0].getBBox().topLeft).objEq(Vec2.of(0, 0), 8); // ± 8
39
+ expect(elems[0].getBBox().bottomRight).objEq(Vec2.of(0, 175), 25); // ± 25
40
+ });
41
+
42
+ it('should draw vertical line with slight bend', () => {
43
+ const editor = createEditor();
44
+
45
+ editor.sendPenEvent(InputEvtType.PointerDownEvt, Vec2.of(417, 24));
46
+ jest.advanceTimersByTime(245);
47
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 197));
48
+ jest.advanceTimersByTime(20);
49
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 199));
50
+ jest.advanceTimersByTime(12);
51
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 201));
52
+ jest.advanceTimersByTime(40);
53
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 203));
54
+ jest.advanceTimersByTime(14);
55
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 206));
56
+ jest.advanceTimersByTime(35);
57
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 208));
58
+ jest.advanceTimersByTime(16);
59
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 211));
60
+ jest.advanceTimersByTime(51);
61
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 215));
62
+ jest.advanceTimersByTime(32);
63
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 218));
64
+ jest.advanceTimersByTime(30);
65
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 220));
66
+ jest.advanceTimersByTime(24);
67
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 222));
68
+ jest.advanceTimersByTime(14);
69
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 224));
70
+ jest.advanceTimersByTime(32);
71
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 227));
72
+ jest.advanceTimersByTime(17);
73
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 229));
74
+ jest.advanceTimersByTime(53);
75
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 234));
76
+ jest.advanceTimersByTime(34);
77
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 236));
78
+ jest.advanceTimersByTime(17);
79
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 238));
80
+ jest.advanceTimersByTime(39);
81
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 240));
82
+ jest.advanceTimersByTime(10);
83
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 243));
84
+ jest.advanceTimersByTime(34);
85
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 250));
86
+ jest.advanceTimersByTime(57);
87
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(423, 252));
88
+ jest.advanceTimersByTime(8);
89
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(422, 256));
90
+ jest.advanceTimersByTime(28);
91
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(422, 258));
92
+ jest.advanceTimersByTime(21);
93
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(421, 262));
94
+ jest.advanceTimersByTime(34);
95
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(420, 264));
96
+ jest.advanceTimersByTime(5);
97
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(420, 266));
98
+ jest.advanceTimersByTime(22);
99
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(420, 268));
100
+ jest.advanceTimersByTime(22);
101
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(420, 271));
102
+ jest.advanceTimersByTime(18);
103
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(420, 274));
104
+ jest.advanceTimersByTime(33);
105
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(420, 277));
106
+ jest.advanceTimersByTime(16);
107
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 279));
108
+ jest.advanceTimersByTime(36);
109
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 282));
110
+ jest.advanceTimersByTime(15);
111
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 284));
112
+ jest.advanceTimersByTime(48);
113
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 289));
114
+ jest.advanceTimersByTime(16);
115
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 291));
116
+ jest.advanceTimersByTime(31);
117
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 295));
118
+ jest.advanceTimersByTime(23);
119
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 301));
120
+ jest.advanceTimersByTime(31);
121
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 306));
122
+ jest.advanceTimersByTime(18);
123
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 308));
124
+ jest.advanceTimersByTime(20);
125
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 310));
126
+ jest.advanceTimersByTime(13);
127
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 313));
128
+ jest.advanceTimersByTime(17);
129
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 317));
130
+ jest.advanceTimersByTime(33);
131
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 321));
132
+ jest.advanceTimersByTime(15);
133
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 324));
134
+ jest.advanceTimersByTime(23);
135
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 326));
136
+ jest.advanceTimersByTime(14);
137
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(419, 329));
138
+ jest.advanceTimersByTime(36);
139
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(420, 333));
140
+ jest.advanceTimersByTime(8);
141
+ editor.sendPenEvent(InputEvtType.PointerMoveEvt, Vec2.of(420, 340));
142
+ editor.sendPenEvent(InputEvtType.PointerUpEvt, Vec2.of(420, 340));
143
+
144
+ const elems = editor.image.getElementsIntersectingRegion(new Rect2(0, 0, 1000, 1000));
145
+ expect(elems).toHaveLength(1);
146
+
147
+ expect(elems[0].getBBox().topLeft).objEq(Vec2.of(420, 24), 8); // ± 8
148
+ expect(elems[0].getBBox().bottomRight).objEq(Vec2.of(420, 340), 25); // ± 25
149
+ });
150
+ });
@@ -12,6 +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/Text';
15
16
 
16
17
  export const cssPrefix = 'selection-tool-';
17
18
 
@@ -272,11 +273,19 @@ export default class SelectionTool extends BaseTool {
272
273
  const sanitize = true;
273
274
  const renderer = new SVGRenderer(exportElem, exportViewport, sanitize);
274
275
 
276
+ const text: string[] = [];
275
277
  for (const elem of selectedElems) {
276
278
  elem.render(renderer);
279
+
280
+ if (elem instanceof TextComponent) {
281
+ text.push(elem.getText());
282
+ }
277
283
  }
278
284
 
279
285
  event.setData('image/svg+xml', exportElem.outerHTML);
286
+ if (text.length > 0) {
287
+ event.setData('text/plain', text.join('\n'));
288
+ }
280
289
  return true;
281
290
  }
282
291
 
package/tsconfig.json CHANGED
@@ -23,10 +23,12 @@
23
23
  "**/node_modules",
24
24
 
25
25
  // Files that don't need transpilation
26
- "**/*.test.ts",
26
+ // "**/*.test.ts", <- vscode requires .test.ts files to be transpiled for other settings to apply.
27
27
  "__mocks__/*",
28
28
 
29
29
  // Output files
30
30
  "./dist/**"
31
31
  ],
32
+
33
+ "files": [ "./src/testing/global.d.ts" ]
32
34
  }