js-draw 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/.github/ISSUE_TEMPLATE/translation.md +4 -1
  2. package/CHANGELOG.md +15 -0
  3. package/dist/bundle.js +1 -1
  4. package/dist/src/Editor.d.ts +4 -1
  5. package/dist/src/Editor.js +117 -2
  6. package/dist/src/EditorImage.js +4 -1
  7. package/dist/src/SVGLoader.d.ts +4 -1
  8. package/dist/src/SVGLoader.js +78 -33
  9. package/dist/src/UndoRedoHistory.d.ts +1 -0
  10. package/dist/src/UndoRedoHistory.js +6 -0
  11. package/dist/src/Viewport.d.ts +1 -0
  12. package/dist/src/Viewport.js +12 -4
  13. package/dist/src/commands/lib.d.ts +2 -1
  14. package/dist/src/commands/lib.js +2 -1
  15. package/dist/src/commands/localization.d.ts +1 -0
  16. package/dist/src/commands/localization.js +1 -0
  17. package/dist/src/commands/uniteCommands.d.ts +4 -0
  18. package/dist/src/commands/uniteCommands.js +105 -0
  19. package/dist/src/components/AbstractComponent.d.ts +2 -0
  20. package/dist/src/components/AbstractComponent.js +41 -5
  21. package/dist/src/components/ImageComponent.d.ts +27 -0
  22. package/dist/src/components/ImageComponent.js +129 -0
  23. package/dist/src/components/Stroke.js +11 -6
  24. package/dist/src/components/builders/FreehandLineBuilder.js +7 -7
  25. package/dist/src/components/lib.d.ts +4 -2
  26. package/dist/src/components/lib.js +4 -2
  27. package/dist/src/components/localization.d.ts +2 -0
  28. package/dist/src/components/localization.js +2 -0
  29. package/dist/src/math/LineSegment2.d.ts +4 -0
  30. package/dist/src/math/LineSegment2.js +9 -0
  31. package/dist/src/math/Path.d.ts +5 -1
  32. package/dist/src/math/Path.js +89 -7
  33. package/dist/src/math/Rect2.js +1 -1
  34. package/dist/src/math/Triangle.d.ts +11 -0
  35. package/dist/src/math/Triangle.js +19 -0
  36. package/dist/src/rendering/Display.js +2 -2
  37. package/dist/src/rendering/localization.d.ts +3 -0
  38. package/dist/src/rendering/localization.js +3 -0
  39. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +9 -1
  40. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
  41. package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
  42. package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
  43. package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
  44. package/dist/src/rendering/renderers/SVGRenderer.d.ts +14 -12
  45. package/dist/src/rendering/renderers/SVGRenderer.js +71 -87
  46. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
  47. package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
  48. package/dist/src/toolbar/HTMLToolbar.d.ts +1 -0
  49. package/dist/src/toolbar/HTMLToolbar.js +1 -0
  50. package/dist/src/toolbar/widgets/BaseWidget.d.ts +3 -0
  51. package/dist/src/toolbar/widgets/BaseWidget.js +21 -1
  52. package/dist/src/tools/BaseTool.d.ts +4 -1
  53. package/dist/src/tools/BaseTool.js +12 -0
  54. package/dist/src/tools/PasteHandler.d.ts +16 -0
  55. package/dist/src/tools/PasteHandler.js +142 -0
  56. package/dist/src/tools/Pen.d.ts +2 -1
  57. package/dist/src/tools/Pen.js +16 -0
  58. package/dist/src/tools/SelectionTool.d.ts +7 -1
  59. package/dist/src/tools/SelectionTool.js +63 -5
  60. package/dist/src/tools/ToolController.d.ts +1 -0
  61. package/dist/src/tools/ToolController.js +45 -29
  62. package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
  63. package/dist/src/tools/ToolSwitcherShortcut.js +26 -0
  64. package/dist/src/tools/lib.d.ts +2 -0
  65. package/dist/src/tools/lib.js +2 -0
  66. package/dist/src/tools/localization.d.ts +4 -0
  67. package/dist/src/tools/localization.js +4 -0
  68. package/dist/src/types.d.ts +21 -4
  69. package/dist/src/types.js +3 -0
  70. package/package.json +2 -2
  71. package/src/Editor.ts +131 -2
  72. package/src/EditorImage.ts +7 -1
  73. package/src/SVGLoader.ts +90 -36
  74. package/src/UndoRedoHistory.test.ts +33 -0
  75. package/src/UndoRedoHistory.ts +8 -0
  76. package/src/Viewport.ts +13 -4
  77. package/src/commands/lib.ts +2 -0
  78. package/src/commands/localization.ts +2 -0
  79. package/src/commands/uniteCommands.test.ts +23 -0
  80. package/src/commands/uniteCommands.ts +121 -0
  81. package/src/components/AbstractComponent.ts +55 -9
  82. package/src/components/ImageComponent.ts +153 -0
  83. package/src/components/Stroke.test.ts +5 -0
  84. package/src/components/Stroke.ts +13 -7
  85. package/src/components/builders/FreehandLineBuilder.ts +7 -7
  86. package/src/components/lib.ts +7 -2
  87. package/src/components/localization.ts +4 -0
  88. package/src/math/LineSegment2.test.ts +9 -0
  89. package/src/math/LineSegment2.ts +13 -0
  90. package/src/math/Path.test.ts +53 -0
  91. package/src/math/Path.toString.test.ts +4 -2
  92. package/src/math/Path.ts +109 -11
  93. package/src/math/Rect2.ts +1 -1
  94. package/src/math/Triangle.ts +29 -0
  95. package/src/rendering/Display.ts +2 -2
  96. package/src/rendering/localization.ts +6 -0
  97. package/src/rendering/renderers/AbstractRenderer.ts +17 -0
  98. package/src/rendering/renderers/CanvasRenderer.ts +10 -1
  99. package/src/rendering/renderers/DummyRenderer.ts +6 -1
  100. package/src/rendering/renderers/SVGRenderer.ts +76 -101
  101. package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
  102. package/src/toolbar/HTMLToolbar.ts +1 -1
  103. package/src/toolbar/types.ts +1 -1
  104. package/src/toolbar/widgets/BaseWidget.ts +27 -1
  105. package/src/tools/BaseTool.ts +17 -1
  106. package/src/tools/PasteHandler.ts +156 -0
  107. package/src/tools/Pen.ts +20 -1
  108. package/src/tools/SelectionTool.ts +80 -8
  109. package/src/tools/ToolController.ts +60 -46
  110. package/src/tools/ToolSwitcherShortcut.ts +34 -0
  111. package/src/tools/lib.ts +2 -0
  112. package/src/tools/localization.ts +10 -0
  113. package/src/types.ts +29 -3
@@ -89,6 +89,52 @@ export default abstract class AbstractComponent {
89
89
  return new AbstractComponent.TransformElementCommand(affineTransfm, this);
90
90
  }
91
91
 
92
+ private static transformElementCommandId = 'transform-element';
93
+
94
+ private static UnresolvedTransformElementCommand = class extends SerializableCommand {
95
+ private command: SerializableCommand|null = null;
96
+
97
+ public constructor(
98
+ private affineTransfm: Mat33,
99
+ private componentID: string,
100
+ ) {
101
+ super(AbstractComponent.transformElementCommandId);
102
+ }
103
+
104
+ private resolveCommand(editor: Editor) {
105
+ if (this.command) {
106
+ return;
107
+ }
108
+
109
+ const component = editor.image.lookupElement(this.componentID);
110
+ if (!component) {
111
+ throw new Error(`Unable to resolve component with ID ${this.componentID}`);
112
+ }
113
+ this.command = new AbstractComponent.TransformElementCommand(this.affineTransfm, component);
114
+ }
115
+
116
+ public apply(editor: Editor) {
117
+ this.resolveCommand(editor);
118
+ this.command!.apply(editor);
119
+ }
120
+
121
+ public unapply(editor: Editor) {
122
+ this.resolveCommand(editor);
123
+ this.command!.unapply(editor);
124
+ }
125
+
126
+ public description(_editor: Editor, localizationTable: EditorLocalization) {
127
+ return localizationTable.transformedElements(1);
128
+ }
129
+
130
+ protected serializeToJSON() {
131
+ return {
132
+ id: this.componentID,
133
+ transfm: this.affineTransfm.toArray(),
134
+ };
135
+ }
136
+ };
137
+
92
138
  private static TransformElementCommand = class extends SerializableCommand {
93
139
  private origZIndex: number;
94
140
 
@@ -96,7 +142,7 @@ export default abstract class AbstractComponent {
96
142
  private affineTransfm: Mat33,
97
143
  private component: AbstractComponent,
98
144
  ) {
99
- super('transform-element');
145
+ super(AbstractComponent.transformElementCommandId);
100
146
  this.origZIndex = component.zIndex;
101
147
  }
102
148
 
@@ -134,21 +180,21 @@ export default abstract class AbstractComponent {
134
180
  }
135
181
 
136
182
  static {
137
- SerializableCommand.register('transform-element', (json: any, editor: Editor) => {
183
+ SerializableCommand.register(AbstractComponent.transformElementCommandId, (json: any, editor: Editor) => {
138
184
  const elem = editor.image.lookupElement(json.id);
139
185
 
140
- if (!elem) {
141
- throw new Error(`Unable to retrieve non-existent element, ${elem}`);
142
- }
143
-
144
- const transform = json.transfm as [
186
+ const transform = new Mat33(...(json.transfm as [
145
187
  number, number, number,
146
188
  number, number, number,
147
189
  number, number, number,
148
- ];
190
+ ]));
191
+
192
+ if (!elem) {
193
+ return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id);
194
+ }
149
195
 
150
196
  return new AbstractComponent.TransformElementCommand(
151
- new Mat33(...transform),
197
+ transform,
152
198
  elem,
153
199
  );
154
200
  });
@@ -0,0 +1,153 @@
1
+ import LineSegment2 from '../math/LineSegment2';
2
+ import Mat33 from '../math/Mat33';
3
+ import Rect2 from '../math/Rect2';
4
+ import AbstractRenderer, { RenderableImage } from '../rendering/renderers/AbstractRenderer';
5
+ import AbstractComponent from './AbstractComponent';
6
+ import { ImageComponentLocalization } from './localization';
7
+
8
+ // Represents a raster image.
9
+ export default class ImageComponent extends AbstractComponent {
10
+ protected contentBBox: Rect2;
11
+ private image: RenderableImage;
12
+
13
+ public constructor(image: RenderableImage) {
14
+ super('image-component');
15
+ this.image = {
16
+ ...image,
17
+ label: image.label ?? image.image.getAttribute('alt') ?? image.image.getAttribute('aria-label') ?? undefined,
18
+ };
19
+
20
+ const isHTMLImageElem = (elem: HTMLCanvasElement|HTMLImageElement): elem is HTMLImageElement => {
21
+ return elem.getAttribute('src') !== undefined;
22
+ };
23
+ if (isHTMLImageElem(image.image) && !image.image.complete) {
24
+ image.image.onload = () => this.recomputeBBox();
25
+ }
26
+
27
+ this.recomputeBBox();
28
+ }
29
+
30
+ private getImageRect() {
31
+ return new Rect2(0, 0, this.image.image.width, this.image.image.height);
32
+ }
33
+
34
+ private recomputeBBox() {
35
+ this.contentBBox = this.getImageRect();
36
+ this.contentBBox = this.contentBBox.transformedBoundingBox(this.image.transform);
37
+ }
38
+
39
+ // Load from an image. Waits for the image to load if incomplete.
40
+ public static async fromImage(elem: HTMLImageElement, transform: Mat33) {
41
+ if (!elem.complete) {
42
+ await new Promise((resolve, reject) => {
43
+ elem.onload = resolve;
44
+ elem.onerror = reject;
45
+ elem.onabort = reject;
46
+ });
47
+ }
48
+
49
+ let width, height;
50
+ if (
51
+ typeof elem.width === 'number' && typeof elem.height === 'number'
52
+ && elem.width !== 0 && elem.height !== 0
53
+ ) {
54
+ width = elem.width as number;
55
+ height = elem.height as number;
56
+ } else {
57
+ width = elem.clientWidth;
58
+ height = elem.clientHeight;
59
+ }
60
+
61
+ let image;
62
+ let url = elem.src ?? '';
63
+ if (!url.startsWith('data:image/')) {
64
+ // Convert to a data URL:
65
+ const canvas = document.createElement('canvas');
66
+ canvas.width = width;
67
+ canvas.height = height;
68
+
69
+ const ctx = canvas.getContext('2d')!;
70
+ ctx.drawImage(elem, 0, 0, canvas.width, canvas.height);
71
+ url = canvas.toDataURL();
72
+ image = canvas;
73
+ } else {
74
+ image = new Image();
75
+ image.src = url;
76
+ image.width = width;
77
+ image.height = height;
78
+ }
79
+
80
+ return new ImageComponent({
81
+ image,
82
+ base64Url: url,
83
+ transform: transform,
84
+ });
85
+ }
86
+
87
+ public render(canvas: AbstractRenderer, _visibleRect?: Rect2): void {
88
+ canvas.drawImage(this.image);
89
+ }
90
+
91
+ public intersects(lineSegment: LineSegment2): boolean {
92
+ const rect = this.getImageRect();
93
+ const edges = rect.getEdges().map(edge => edge.transformedBy(this.image.transform));
94
+ for (const edge of edges) {
95
+ if (edge.intersects(lineSegment)) {
96
+ return true;
97
+ }
98
+ }
99
+ return false;
100
+ }
101
+
102
+ protected serializeToJSON() {
103
+ return {
104
+ src: this.image.base64Url,
105
+ label: this.image.label,
106
+
107
+ // Store the width and height for bounding box computations while the image is loading.
108
+ width: this.image.image.width,
109
+ height: this.image.image.height,
110
+
111
+ transform: this.image.transform.toArray(),
112
+ };
113
+ }
114
+
115
+ protected applyTransformation(affineTransfm: Mat33) {
116
+ this.image.transform = affineTransfm.rightMul(this.image.transform);
117
+ this.recomputeBBox();
118
+ }
119
+
120
+ public description(localizationTable: ImageComponentLocalization): string {
121
+ return this.image.label ? localizationTable.imageNode(this.image.label) : localizationTable.unlabeledImageNode;
122
+ }
123
+
124
+ protected createClone(): AbstractComponent {
125
+ return new ImageComponent({
126
+ ...this.image,
127
+ });
128
+ }
129
+
130
+ public static deserializeFromJSON(data: any): ImageComponent {
131
+ if (!(typeof data.src === 'string')) {
132
+ throw new Error(`${data} has invalid format! Expected src property.`);
133
+ }
134
+
135
+ const image = new Image();
136
+ image.src = data.src;
137
+ image.width = data.width;
138
+ image.height = data.height;
139
+
140
+ return new ImageComponent({
141
+ image: image,
142
+ base64Url: image.src,
143
+ label: data.label,
144
+ transform: new Mat33(...(data.transform as [
145
+ number, number, number,
146
+ number, number, number,
147
+ number, number, number,
148
+ ])),
149
+ });
150
+ }
151
+ }
152
+
153
+ AbstractComponent.registerComponent('image-component', ImageComponent.deserializeFromJSON);
@@ -64,6 +64,11 @@ describe('Stroke', () => {
64
64
  "path": "m0,0 l10,10z"
65
65
  }
66
66
  ]`);
67
+ const path = deserialized.getPath();
68
+
69
+ // Should cache the original string representation.
70
+ expect(deserialized.getPath().toString()).toBe('m0,0 l10,10z');
71
+ path['cachedStringVersion'] = null;
67
72
  expect(deserialized.getPath().toString()).toBe('M0,0L10,10L0,0');
68
73
  });
69
74
  });
@@ -9,7 +9,6 @@ import { ImageComponentLocalization } from './localization';
9
9
 
10
10
  interface StrokePart extends RenderablePathSpec {
11
11
  path: Path;
12
- bbox: Rect2;
13
12
  }
14
13
 
15
14
  export default class Stroke extends AbstractComponent {
@@ -19,7 +18,7 @@ export default class Stroke extends AbstractComponent {
19
18
  public constructor(parts: RenderablePathSpec[]) {
20
19
  super('stroke');
21
20
 
22
- this.parts = parts.map(section => {
21
+ this.parts = parts.map((section): StrokePart => {
23
22
  const path = Path.fromRenderable(section);
24
23
  const pathBBox = this.bboxForPart(path.bbox, section.style);
25
24
 
@@ -31,7 +30,6 @@ export default class Stroke extends AbstractComponent {
31
30
 
32
31
  return {
33
32
  path,
34
- bbox: pathBBox,
35
33
 
36
34
  // To implement RenderablePathSpec
37
35
  startPoint: path.startPoint,
@@ -54,10 +52,19 @@ export default class Stroke extends AbstractComponent {
54
52
  public render(canvas: AbstractRenderer, visibleRect?: Rect2): void {
55
53
  canvas.startObject(this.getBBox());
56
54
  for (const part of this.parts) {
57
- const bbox = part.bbox;
58
- if (!visibleRect || bbox.intersects(visibleRect)) {
59
- canvas.drawPath(part);
55
+ const bbox = this.bboxForPart(part.path.bbox, part.style);
56
+ if (visibleRect) {
57
+ if (!bbox.intersects(visibleRect)) {
58
+ continue;
59
+ }
60
+
61
+ const muchBiggerThanVisible = bbox.size.x > visibleRect.size.x * 2 || bbox.size.y > visibleRect.size.y * 2;
62
+ if (muchBiggerThanVisible && !part.path.closedRoughlyIntersects(visibleRect)) {
63
+ continue;
64
+ }
60
65
  }
66
+
67
+ canvas.drawPath(part);
61
68
  }
62
69
  canvas.endObject(this.getLoadSaveData());
63
70
  }
@@ -89,7 +96,6 @@ export default class Stroke extends AbstractComponent {
89
96
 
90
97
  return {
91
98
  path: newPath,
92
- bbox: newBBox,
93
99
  startPoint: newPath.startPoint,
94
100
  commands: newPath.parts,
95
101
  style: part.style,
@@ -12,9 +12,9 @@ import RenderingStyle from '../../rendering/RenderingStyle';
12
12
 
13
13
  export const makeFreehandLineBuilder: ComponentBuilderFactory = (initialPoint: StrokeDataPoint, viewport: Viewport) => {
14
14
  // Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
15
- // less than ± 2 px from the curve.
15
+ // less than ±1 px from the curve.
16
16
  const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 7;
17
- const minSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 2;
17
+ const minSmoothingDist = viewport.getSizeOfPixelOnCanvas();
18
18
 
19
19
  return new FreehandLineBuilder(
20
20
  initialPoint, minSmoothingDist, maxSmoothingDist
@@ -197,7 +197,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
197
197
  return;
198
198
  }
199
199
 
200
- const width = Viewport.roundPoint(this.startPoint.width / 3.5, this.minFitAllowed);
200
+ const width = Viewport.roundPoint(this.startPoint.width / 3.5, Math.min(this.minFitAllowed, this.startPoint.width / 4));
201
201
  const center = this.roundPoint(this.startPoint.pos);
202
202
 
203
203
  // Start on the right, cycle clockwise:
@@ -329,9 +329,9 @@ export default class FreehandLineBuilder implements ComponentBuilder {
329
329
  return upperBoundary.intersects(lowerBoundary).length > 0;
330
330
  };
331
331
 
332
- // If the boundaries have two intersections, increasing the half vector's length could fix this.
332
+ // If the boundaries have intersections, increasing the half vector's length could fix this.
333
333
  if (boundariesIntersect()) {
334
- halfVec = halfVec.times(2);
334
+ halfVec = halfVec.times(1.1);
335
335
  }
336
336
 
337
337
  // Each starts at startPt ± startVec
@@ -492,7 +492,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
492
492
  const dist = proj.minus(point).magnitude();
493
493
 
494
494
  const minFit = Math.max(
495
- Math.min(this.curveStartWidth, this.curveEndWidth) / 2,
495
+ Math.min(this.curveStartWidth, this.curveEndWidth) / 3,
496
496
  this.minFitAllowed
497
497
  );
498
498
  if (dist > minFit || dist > this.maxFitAllowed) {
@@ -503,7 +503,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
503
503
  };
504
504
 
505
505
  const approxCurveLen = controlPoint.minus(segmentStart).magnitude() + segmentEnd.minus(controlPoint).magnitude();
506
- if (this.buffer.length > 3 && approxCurveLen > this.curveEndWidth / 2) {
506
+ if (this.buffer.length > 3 && approxCurveLen > this.curveEndWidth / 3) {
507
507
  if (!curveMatchesPoints(this.currentCurve)) {
508
508
  // Use a curve that better fits the points
509
509
  this.currentCurve = prevCurve;
@@ -1,12 +1,17 @@
1
1
  export * from './builders/types';
2
2
  export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
3
3
 
4
- import AbstractComponent from './AbstractComponent';
4
+ export * from './AbstractComponent';
5
+ export { default as AbstractComponent } from './AbstractComponent';
5
6
  import Stroke from './Stroke';
6
7
  import Text from './Text';
8
+ import ImageComponent from './ImageComponent';
7
9
 
8
10
  export {
9
- AbstractComponent,
10
11
  Stroke,
11
12
  Text,
13
+
14
+ Text as TextComponent,
15
+ Stroke as StrokeComponent,
16
+ ImageComponent,
12
17
  };
@@ -1,11 +1,15 @@
1
1
  export interface ImageComponentLocalization {
2
+ unlabeledImageNode: string;
2
3
  text: (text: string)=> string;
4
+ imageNode: (description: string)=> string;
3
5
  stroke: string;
4
6
  svgObject: string;
5
7
  }
6
8
 
7
9
  export const defaultComponentLocalization: ImageComponentLocalization = {
10
+ unlabeledImageNode: 'Unlabeled image node',
8
11
  stroke: 'Stroke',
9
12
  svgObject: 'SVG Object',
10
13
  text: (text) => `Text object: ${text}`,
14
+ imageNode: (description: string) => `Image: ${description}`,
11
15
  };
@@ -1,6 +1,7 @@
1
1
  import LineSegment2 from './LineSegment2';
2
2
  import { loadExpectExtensions } from '../testing/loadExpectExtensions';
3
3
  import { Vec2 } from './Vec2';
4
+ import Mat33 from './Mat33';
4
5
 
5
6
  loadExpectExtensions();
6
7
 
@@ -89,4 +90,12 @@ describe('Line2', () => {
89
90
  const line = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 4));
90
91
  expect(line.closestPointTo(Vec2.of(5, 2))).objEq(Vec2.of(2, 4));
91
92
  });
93
+
94
+ it('Should translate when translated by a translation matrix', () => {
95
+ const line = new LineSegment2(Vec2.of(-1, 1), Vec2.of(2, 100));
96
+ expect(line.transformedBy(Mat33.translation(Vec2.of(1, -2)))).toMatchObject({
97
+ p1: Vec2.of(0, -1),
98
+ p2: Vec2.of(3, 98),
99
+ });
100
+ });
92
101
  });
@@ -1,3 +1,4 @@
1
+ import Mat33 from './Mat33';
1
2
  import Rect2 from './Rect2';
2
3
  import { Vec2, Point2 } from './Vec2';
3
4
 
@@ -126,6 +127,10 @@ export default class LineSegment2 {
126
127
  };
127
128
  }
128
129
 
130
+ public intersects(other: LineSegment2) {
131
+ return this.intersection(other) !== null;
132
+ }
133
+
129
134
  // Returns the closest point on this to [target]
130
135
  public closestPointTo(target: Point2) {
131
136
  // Distance from P1 along this' direction.
@@ -144,4 +149,12 @@ export default class LineSegment2 {
144
149
  return this.p1;
145
150
  }
146
151
  }
152
+
153
+ public transformedBy(affineTransfm: Mat33): LineSegment2 {
154
+ return new LineSegment2(affineTransfm.transformVec2(this.p1), affineTransfm.transformVec2(this.p2));
155
+ }
156
+
157
+ public toString() {
158
+ return `LineSegment(${this.p1.toString()}, ${this.p2.toString()})`;
159
+ }
147
160
  }
@@ -1,6 +1,7 @@
1
1
  import { Bezier } from 'bezier-js';
2
2
  import LineSegment2 from './LineSegment2';
3
3
  import Path, { PathCommandType } from './Path';
4
+ import Rect2 from './Rect2';
4
5
  import { Vec2 } from './Vec2';
5
6
 
6
7
  describe('Path', () => {
@@ -93,4 +94,56 @@ describe('Path', () => {
93
94
  y: 100,
94
95
  });
95
96
  });
97
+
98
+ describe('polylineApproximation', () => {
99
+ it('should approximate Bézier curves with polylines', () => {
100
+ const path = Path.fromString('m0,0 l4,4 Q 1,4 4,1z');
101
+
102
+ expect(path.polylineApproximation()).toMatchObject([
103
+ new LineSegment2(Vec2.of(0, 0), Vec2.of(4, 4)),
104
+ new LineSegment2(Vec2.of(4, 4), Vec2.of(1, 4)),
105
+ new LineSegment2(Vec2.of(1, 4), Vec2.of(4, 1)),
106
+ new LineSegment2(Vec2.of(4, 1), Vec2.of(0, 0)),
107
+ ]);
108
+ });
109
+ });
110
+
111
+ describe('roughlyIntersectsClosed', () => {
112
+ it('small, line-only path', () => {
113
+ const path = Path.fromString('m0,0 l10,10 L0,10 z');
114
+ expect(
115
+ path.closedRoughlyIntersects(Rect2.fromCorners(Vec2.zero, Vec2.of(20, 20)))
116
+ ).toBe(true);
117
+ expect(
118
+ path.closedRoughlyIntersects(Rect2.fromCorners(Vec2.zero, Vec2.of(2, 2)))
119
+ ).toBe(true);
120
+ expect(
121
+ path.closedRoughlyIntersects(new Rect2(10, 1, 1, 1))
122
+ ).toBe(false);
123
+ expect(
124
+ path.closedRoughlyIntersects(new Rect2(1, 5, 1, 1))
125
+ ).toBe(true);
126
+ });
127
+
128
+ it('path with Bézier curves', () => {
129
+ const path = Path.fromString(`
130
+ M1090,2560
131
+ L1570,2620
132
+ Q1710,1300 1380,720
133
+ Q980,100 -460,-640
134
+ L-680,-200
135
+ Q670,470 960,980
136
+ Q1230,1370 1090,2560
137
+ `);
138
+ expect(
139
+ path.closedRoughlyIntersects(new Rect2(0, 0, 500, 500))
140
+ ).toBe(true);
141
+ expect(
142
+ path.closedRoughlyIntersects(new Rect2(0, 0, 5, 5))
143
+ ).toBe(true);
144
+ expect(
145
+ path.closedRoughlyIntersects(new Rect2(-10000, 0, 500, 500))
146
+ ).toBe(false);
147
+ });
148
+ });
96
149
  });
@@ -42,13 +42,15 @@ describe('Path.toString', () => {
42
42
  },
43
43
  ]);
44
44
 
45
- expect(path.toString()).toBe('M1000,2000000L30.0001,40');
45
+ expect(path.toString()).toBe('M1000,2000000l-970-1999960');
46
46
  });
47
47
 
48
48
  it('deserialized path should serialize to the same/similar path, but with rounded components', () => {
49
49
  const path1 = Path.fromString('M100,100 L101,101 Q102,102 90.000000001,89.99999999 Z');
50
+ path1['cachedStringVersion'] = null; // Clear the cache.
51
+
50
52
  expect(path1.toString()).toBe([
51
- 'M100,100', 'L101,101', 'Q102,102 90,90', 'L100,100'
53
+ 'M100,100', 'l1,1', 'q1,1 -11-11', 'l10,10'
52
54
  ].join(''));
53
55
  });
54
56
  });