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
@@ -113,9 +113,45 @@ export default class AbstractComponent {
113
113
  // Topmost z-index
114
114
  AbstractComponent.zIndexCounter = 0;
115
115
  AbstractComponent.deserializationCallbacks = {};
116
+ AbstractComponent.transformElementCommandId = 'transform-element';
117
+ AbstractComponent.UnresolvedTransformElementCommand = class extends SerializableCommand {
118
+ constructor(affineTransfm, componentID) {
119
+ super(AbstractComponent.transformElementCommandId);
120
+ this.affineTransfm = affineTransfm;
121
+ this.componentID = componentID;
122
+ this.command = null;
123
+ }
124
+ resolveCommand(editor) {
125
+ if (this.command) {
126
+ return;
127
+ }
128
+ const component = editor.image.lookupElement(this.componentID);
129
+ if (!component) {
130
+ throw new Error(`Unable to resolve component with ID ${this.componentID}`);
131
+ }
132
+ this.command = new AbstractComponent.TransformElementCommand(this.affineTransfm, component);
133
+ }
134
+ apply(editor) {
135
+ this.resolveCommand(editor);
136
+ this.command.apply(editor);
137
+ }
138
+ unapply(editor) {
139
+ this.resolveCommand(editor);
140
+ this.command.unapply(editor);
141
+ }
142
+ description(_editor, localizationTable) {
143
+ return localizationTable.transformedElements(1);
144
+ }
145
+ serializeToJSON() {
146
+ return {
147
+ id: this.componentID,
148
+ transfm: this.affineTransfm.toArray(),
149
+ };
150
+ }
151
+ };
116
152
  AbstractComponent.TransformElementCommand = (_a = class extends SerializableCommand {
117
153
  constructor(affineTransfm, component) {
118
- super('transform-element');
154
+ super(AbstractComponent.transformElementCommandId);
119
155
  this.affineTransfm = affineTransfm;
120
156
  this.component = component;
121
157
  this.origZIndex = component.zIndex;
@@ -155,13 +191,13 @@ AbstractComponent.TransformElementCommand = (_a = class extends SerializableComm
155
191
  }
156
192
  },
157
193
  (() => {
158
- SerializableCommand.register('transform-element', (json, editor) => {
194
+ SerializableCommand.register(AbstractComponent.transformElementCommandId, (json, editor) => {
159
195
  const elem = editor.image.lookupElement(json.id);
196
+ const transform = new Mat33(...json.transfm);
160
197
  if (!elem) {
161
- throw new Error(`Unable to retrieve non-existent element, ${elem}`);
198
+ return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id);
162
199
  }
163
- const transform = json.transfm;
164
- return new AbstractComponent.TransformElementCommand(new Mat33(...transform), elem);
200
+ return new AbstractComponent.TransformElementCommand(transform, elem);
165
201
  });
166
202
  })(),
167
203
  _a);
@@ -0,0 +1,27 @@
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
+ export default class ImageComponent extends AbstractComponent {
8
+ protected contentBBox: Rect2;
9
+ private image;
10
+ constructor(image: RenderableImage);
11
+ private getImageRect;
12
+ private recomputeBBox;
13
+ static fromImage(elem: HTMLImageElement, transform: Mat33): Promise<ImageComponent>;
14
+ render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
15
+ intersects(lineSegment: LineSegment2): boolean;
16
+ protected serializeToJSON(): {
17
+ src: string;
18
+ label: string | undefined;
19
+ width: number;
20
+ height: number;
21
+ transform: number[];
22
+ };
23
+ protected applyTransformation(affineTransfm: Mat33): void;
24
+ description(localizationTable: ImageComponentLocalization): string;
25
+ protected createClone(): AbstractComponent;
26
+ static deserializeFromJSON(data: any): ImageComponent;
27
+ }
@@ -0,0 +1,129 @@
1
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
2
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
3
+ return new (P || (P = Promise))(function (resolve, reject) {
4
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
5
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
6
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
7
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
8
+ });
9
+ };
10
+ import Mat33 from '../math/Mat33';
11
+ import Rect2 from '../math/Rect2';
12
+ import AbstractComponent from './AbstractComponent';
13
+ // Represents a raster image.
14
+ export default class ImageComponent extends AbstractComponent {
15
+ constructor(image) {
16
+ var _a, _b, _c;
17
+ super('image-component');
18
+ this.image = Object.assign(Object.assign({}, image), { label: (_c = (_b = (_a = image.label) !== null && _a !== void 0 ? _a : image.image.getAttribute('alt')) !== null && _b !== void 0 ? _b : image.image.getAttribute('aria-label')) !== null && _c !== void 0 ? _c : undefined });
19
+ const isHTMLImageElem = (elem) => {
20
+ return elem.getAttribute('src') !== undefined;
21
+ };
22
+ if (isHTMLImageElem(image.image) && !image.image.complete) {
23
+ image.image.onload = () => this.recomputeBBox();
24
+ }
25
+ this.recomputeBBox();
26
+ }
27
+ getImageRect() {
28
+ return new Rect2(0, 0, this.image.image.width, this.image.image.height);
29
+ }
30
+ recomputeBBox() {
31
+ this.contentBBox = this.getImageRect();
32
+ this.contentBBox = this.contentBBox.transformedBoundingBox(this.image.transform);
33
+ }
34
+ // Load from an image. Waits for the image to load if incomplete.
35
+ static fromImage(elem, transform) {
36
+ var _a;
37
+ return __awaiter(this, void 0, void 0, function* () {
38
+ if (!elem.complete) {
39
+ yield new Promise((resolve, reject) => {
40
+ elem.onload = resolve;
41
+ elem.onerror = reject;
42
+ elem.onabort = reject;
43
+ });
44
+ }
45
+ let width, height;
46
+ if (typeof elem.width === 'number' && typeof elem.height === 'number'
47
+ && elem.width !== 0 && elem.height !== 0) {
48
+ width = elem.width;
49
+ height = elem.height;
50
+ }
51
+ else {
52
+ width = elem.clientWidth;
53
+ height = elem.clientHeight;
54
+ }
55
+ let image;
56
+ let url = (_a = elem.src) !== null && _a !== void 0 ? _a : '';
57
+ if (!url.startsWith('data:image/')) {
58
+ // Convert to a data URL:
59
+ const canvas = document.createElement('canvas');
60
+ canvas.width = width;
61
+ canvas.height = height;
62
+ const ctx = canvas.getContext('2d');
63
+ ctx.drawImage(elem, 0, 0, canvas.width, canvas.height);
64
+ url = canvas.toDataURL();
65
+ image = canvas;
66
+ }
67
+ else {
68
+ image = new Image();
69
+ image.src = url;
70
+ image.width = width;
71
+ image.height = height;
72
+ }
73
+ return new ImageComponent({
74
+ image,
75
+ base64Url: url,
76
+ transform: transform,
77
+ });
78
+ });
79
+ }
80
+ render(canvas, _visibleRect) {
81
+ canvas.drawImage(this.image);
82
+ }
83
+ intersects(lineSegment) {
84
+ const rect = this.getImageRect();
85
+ const edges = rect.getEdges().map(edge => edge.transformedBy(this.image.transform));
86
+ for (const edge of edges) {
87
+ if (edge.intersects(lineSegment)) {
88
+ return true;
89
+ }
90
+ }
91
+ return false;
92
+ }
93
+ serializeToJSON() {
94
+ return {
95
+ src: this.image.base64Url,
96
+ label: this.image.label,
97
+ // Store the width and height for bounding box computations while the image is loading.
98
+ width: this.image.image.width,
99
+ height: this.image.image.height,
100
+ transform: this.image.transform.toArray(),
101
+ };
102
+ }
103
+ applyTransformation(affineTransfm) {
104
+ this.image.transform = affineTransfm.rightMul(this.image.transform);
105
+ this.recomputeBBox();
106
+ }
107
+ description(localizationTable) {
108
+ return this.image.label ? localizationTable.imageNode(this.image.label) : localizationTable.unlabeledImageNode;
109
+ }
110
+ createClone() {
111
+ return new ImageComponent(Object.assign({}, this.image));
112
+ }
113
+ static deserializeFromJSON(data) {
114
+ if (!(typeof data.src === 'string')) {
115
+ throw new Error(`${data} has invalid format! Expected src property.`);
116
+ }
117
+ const image = new Image();
118
+ image.src = data.src;
119
+ image.width = data.width;
120
+ image.height = data.height;
121
+ return new ImageComponent({
122
+ image: image,
123
+ base64Url: image.src,
124
+ label: data.label,
125
+ transform: new Mat33(...data.transform),
126
+ });
127
+ }
128
+ }
129
+ AbstractComponent.registerComponent('image-component', ImageComponent.deserializeFromJSON);
@@ -6,7 +6,7 @@ export default class Stroke extends AbstractComponent {
6
6
  constructor(parts) {
7
7
  var _a;
8
8
  super('stroke');
9
- this.parts = parts.map(section => {
9
+ this.parts = parts.map((section) => {
10
10
  const path = Path.fromRenderable(section);
11
11
  const pathBBox = this.bboxForPart(path.bbox, section.style);
12
12
  if (!this.contentBBox) {
@@ -17,7 +17,6 @@ export default class Stroke extends AbstractComponent {
17
17
  }
18
18
  return {
19
19
  path,
20
- bbox: pathBBox,
21
20
  // To implement RenderablePathSpec
22
21
  startPoint: path.startPoint,
23
22
  style: section.style,
@@ -37,10 +36,17 @@ export default class Stroke extends AbstractComponent {
37
36
  render(canvas, visibleRect) {
38
37
  canvas.startObject(this.getBBox());
39
38
  for (const part of this.parts) {
40
- const bbox = part.bbox;
41
- if (!visibleRect || bbox.intersects(visibleRect)) {
42
- canvas.drawPath(part);
39
+ const bbox = this.bboxForPart(part.path.bbox, part.style);
40
+ if (visibleRect) {
41
+ if (!bbox.intersects(visibleRect)) {
42
+ continue;
43
+ }
44
+ const muchBiggerThanVisible = bbox.size.x > visibleRect.size.x * 2 || bbox.size.y > visibleRect.size.y * 2;
45
+ if (muchBiggerThanVisible && !part.path.closedRoughlyIntersects(visibleRect)) {
46
+ continue;
47
+ }
43
48
  }
49
+ canvas.drawPath(part);
44
50
  }
45
51
  canvas.endObject(this.getLoadSaveData());
46
52
  }
@@ -67,7 +73,6 @@ export default class Stroke extends AbstractComponent {
67
73
  }
68
74
  return {
69
75
  path: newPath,
70
- bbox: newBBox,
71
76
  startPoint: newPath.startPoint,
72
77
  commands: newPath.parts,
73
78
  style: part.style,
@@ -7,9 +7,9 @@ import Stroke from '../Stroke';
7
7
  import Viewport from '../../Viewport';
8
8
  export const makeFreehandLineBuilder = (initialPoint, viewport) => {
9
9
  // Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
10
- // less than ± 2 px from the curve.
10
+ // less than ±1 px from the curve.
11
11
  const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 7;
12
- const minSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 2;
12
+ const minSmoothingDist = viewport.getSizeOfPixelOnCanvas();
13
13
  return new FreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist);
14
14
  };
15
15
  // Handles stroke smoothing and creates Strokes from user/stylus input.
@@ -136,7 +136,7 @@ export default class FreehandLineBuilder {
136
136
  if (!this.isFirstSegment) {
137
137
  return;
138
138
  }
139
- const width = Viewport.roundPoint(this.startPoint.width / 3.5, this.minFitAllowed);
139
+ const width = Viewport.roundPoint(this.startPoint.width / 3.5, Math.min(this.minFitAllowed, this.startPoint.width / 4));
140
140
  const center = this.roundPoint(this.startPoint.pos);
141
141
  // Start on the right, cycle clockwise:
142
142
  // |
@@ -237,9 +237,9 @@ export default class FreehandLineBuilder {
237
237
  const lowerBoundary = computeBoundaryCurve(-1, halfVec);
238
238
  return upperBoundary.intersects(lowerBoundary).length > 0;
239
239
  };
240
- // If the boundaries have two intersections, increasing the half vector's length could fix this.
240
+ // If the boundaries have intersections, increasing the half vector's length could fix this.
241
241
  if (boundariesIntersect()) {
242
- halfVec = halfVec.times(2);
242
+ halfVec = halfVec.times(1.1);
243
243
  }
244
244
  // Each starts at startPt ± startVec
245
245
  const lowerCurve = {
@@ -362,7 +362,7 @@ export default class FreehandLineBuilder {
362
362
  for (const point of this.buffer) {
363
363
  const proj = Vec2.ofXY(curve.project(point.xy));
364
364
  const dist = proj.minus(point).magnitude();
365
- const minFit = Math.max(Math.min(this.curveStartWidth, this.curveEndWidth) / 2, this.minFitAllowed);
365
+ const minFit = Math.max(Math.min(this.curveStartWidth, this.curveEndWidth) / 3, this.minFitAllowed);
366
366
  if (dist > minFit || dist > this.maxFitAllowed) {
367
367
  return false;
368
368
  }
@@ -370,7 +370,7 @@ export default class FreehandLineBuilder {
370
370
  return true;
371
371
  };
372
372
  const approxCurveLen = controlPoint.minus(segmentStart).magnitude() + segmentEnd.minus(controlPoint).magnitude();
373
- if (this.buffer.length > 3 && approxCurveLen > this.curveEndWidth / 2) {
373
+ if (this.buffer.length > 3 && approxCurveLen > this.curveEndWidth / 3) {
374
374
  if (!curveMatchesPoints(this.currentCurve)) {
375
375
  // Use a curve that better fits the points
376
376
  this.currentCurve = prevCurve;
@@ -1,6 +1,8 @@
1
1
  export * from './builders/types';
2
2
  export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
3
- import AbstractComponent from './AbstractComponent';
3
+ export * from './AbstractComponent';
4
+ export { default as AbstractComponent } from './AbstractComponent';
4
5
  import Stroke from './Stroke';
5
6
  import Text from './Text';
6
- export { AbstractComponent, Stroke, Text, };
7
+ import ImageComponent from './ImageComponent';
8
+ export { Stroke, Text, Text as TextComponent, Stroke as StrokeComponent, ImageComponent, };
@@ -1,6 +1,8 @@
1
1
  export * from './builders/types';
2
2
  export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
3
- import AbstractComponent from './AbstractComponent';
3
+ export * from './AbstractComponent';
4
+ export { default as AbstractComponent } from './AbstractComponent';
4
5
  import Stroke from './Stroke';
5
6
  import Text from './Text';
6
- export { AbstractComponent, Stroke, Text, };
7
+ import ImageComponent from './ImageComponent';
8
+ export { Stroke, Text, Text as TextComponent, Stroke as StrokeComponent, ImageComponent, };
@@ -1,5 +1,7 @@
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
  }
@@ -1,5 +1,7 @@
1
1
  export const defaultComponentLocalization = {
2
+ unlabeledImageNode: 'Unlabeled image node',
2
3
  stroke: 'Stroke',
3
4
  svgObject: 'SVG Object',
4
5
  text: (text) => `Text object: ${text}`,
6
+ imageNode: (description) => `Image: ${description}`,
5
7
  };
@@ -1,3 +1,4 @@
1
+ import Mat33 from './Mat33';
1
2
  import Rect2 from './Rect2';
2
3
  import { Vec2, Point2 } from './Vec2';
3
4
  interface IntersectionResult {
@@ -15,6 +16,9 @@ export default class LineSegment2 {
15
16
  get p2(): Point2;
16
17
  get(t: number): Point2;
17
18
  intersection(other: LineSegment2): IntersectionResult | null;
19
+ intersects(other: LineSegment2): boolean;
18
20
  closestPointTo(target: Point2): import("./Vec3").default;
21
+ transformedBy(affineTransfm: Mat33): LineSegment2;
22
+ toString(): string;
19
23
  }
20
24
  export {};
@@ -97,6 +97,9 @@ export default class LineSegment2 {
97
97
  t: resultT,
98
98
  };
99
99
  }
100
+ intersects(other) {
101
+ return this.intersection(other) !== null;
102
+ }
100
103
  // Returns the closest point on this to [target]
101
104
  closestPointTo(target) {
102
105
  // Distance from P1 along this' direction.
@@ -113,4 +116,10 @@ export default class LineSegment2 {
113
116
  return this.p1;
114
117
  }
115
118
  }
119
+ transformedBy(affineTransfm) {
120
+ return new LineSegment2(affineTransfm.transformVec2(this.p1), affineTransfm.transformVec2(this.p2));
121
+ }
122
+ toString() {
123
+ return `LineSegment(${this.p1.toString()}, ${this.p2.toString()})`;
124
+ }
116
125
  }
@@ -39,18 +39,22 @@ interface IntersectionResult {
39
39
  export default class Path {
40
40
  readonly startPoint: Point2;
41
41
  readonly parts: PathCommand[];
42
- private cachedGeometry;
43
42
  readonly bbox: Rect2;
44
43
  constructor(startPoint: Point2, parts: PathCommand[]);
44
+ private cachedGeometry;
45
45
  get geometry(): Array<LineSegment2 | Bezier>;
46
+ private cachedPolylineApproximation;
47
+ polylineApproximation(): LineSegment2[];
46
48
  static computeBBoxForSegment(startPoint: Point2, part: PathCommand): Rect2;
47
49
  intersection(line: LineSegment2): IntersectionResult[];
48
50
  mapPoints(mapping: (point: Point2) => Point2): Path;
49
51
  transformedBy(affineTransfm: Mat33): Path;
50
52
  union(other: Path | null): Path;
53
+ closedRoughlyIntersects(rect: Rect2): boolean;
51
54
  static fromRect(rect: Rect2, lineWidth?: number | null): Path;
52
55
  static fromRenderable(renderable: RenderablePathSpec): Path;
53
56
  toRenderable(fill: RenderingStyle): RenderablePathSpec;
57
+ private cachedStringVersion;
54
58
  toString(): string;
55
59
  serialize(): string;
56
60
  static toString(startPoint: Point2, parts: PathCommand[], onlyAbsCommands?: boolean): string;
@@ -15,6 +15,8 @@ export default class Path {
15
15
  this.startPoint = startPoint;
16
16
  this.parts = parts;
17
17
  this.cachedGeometry = null;
18
+ this.cachedPolylineApproximation = null;
19
+ this.cachedStringVersion = null;
18
20
  // Initial bounding box contains one point: the start point.
19
21
  this.bbox = Rect2.bboxOf([startPoint]);
20
22
  // Convert into a representation of the geometry (cache for faster intersection
@@ -52,6 +54,34 @@ export default class Path {
52
54
  this.cachedGeometry = geometry;
53
55
  return this.cachedGeometry;
54
56
  }
57
+ // Approximates this path with a group of line segments.
58
+ polylineApproximation() {
59
+ if (this.cachedPolylineApproximation) {
60
+ return this.cachedPolylineApproximation;
61
+ }
62
+ const points = [];
63
+ for (const part of this.parts) {
64
+ switch (part.kind) {
65
+ case PathCommandType.CubicBezierTo:
66
+ points.push(part.controlPoint1, part.controlPoint2, part.endPoint);
67
+ break;
68
+ case PathCommandType.QuadraticBezierTo:
69
+ points.push(part.controlPoint, part.endPoint);
70
+ break;
71
+ case PathCommandType.MoveTo:
72
+ case PathCommandType.LineTo:
73
+ points.push(part.point);
74
+ break;
75
+ }
76
+ }
77
+ const result = [];
78
+ let prevPoint = this.startPoint;
79
+ for (const point of points) {
80
+ result.push(new LineSegment2(prevPoint, point));
81
+ prevPoint = point;
82
+ }
83
+ return result;
84
+ }
55
85
  static computeBBoxForSegment(startPoint, part) {
56
86
  const points = [startPoint];
57
87
  let exhaustivenessCheck;
@@ -73,6 +103,9 @@ export default class Path {
73
103
  return Rect2.bboxOf(points);
74
104
  }
75
105
  intersection(line) {
106
+ if (!line.bbox.intersects(this.bbox)) {
107
+ return [];
108
+ }
76
109
  const result = [];
77
110
  for (const part of this.geometry) {
78
111
  if (part instanceof LineSegment2) {
@@ -162,6 +195,47 @@ export default class Path {
162
195
  ...other.parts,
163
196
  ]);
164
197
  }
198
+ // Treats this as a closed path and returns true if part of `rect` is roughly within
199
+ // this path's interior.
200
+ //
201
+ // Note: Assumes that this is a closed, non-self-intersecting path.
202
+ closedRoughlyIntersects(rect) {
203
+ if (rect.containsRect(this.bbox)) {
204
+ return true;
205
+ }
206
+ // Choose a point outside of the path.
207
+ const startPt = this.bbox.topLeft.minus(Vec2.of(1, 1));
208
+ const testPts = rect.corners;
209
+ const polygon = this.polylineApproximation();
210
+ for (const point of testPts) {
211
+ const testLine = new LineSegment2(point, startPt);
212
+ let intersectionCount = 0;
213
+ for (const line of polygon) {
214
+ if (line.intersects(testLine)) {
215
+ intersectionCount++;
216
+ }
217
+ }
218
+ // Odd? The point is within the polygon!
219
+ if (intersectionCount % 2 === 1) {
220
+ return true;
221
+ }
222
+ }
223
+ // Grow the rectangle for possible additional precision.
224
+ const grownRect = rect.grownBy(Math.min(rect.size.x, rect.size.y));
225
+ const edges = [];
226
+ for (const subrect of grownRect.divideIntoGrid(4, 4)) {
227
+ edges.push(...subrect.getEdges());
228
+ }
229
+ for (const edge of edges) {
230
+ for (const line of polygon) {
231
+ if (edge.intersects(line)) {
232
+ return true;
233
+ }
234
+ }
235
+ }
236
+ // Even? Probably no intersection.
237
+ return false;
238
+ }
165
239
  // Returns a path that outlines [rect]. If [lineWidth] is not given, the resultant path is
166
240
  // the outline of [rect]. Otherwise, the resultant path represents a line of width [lineWidth]
167
241
  // that traces [rect].
@@ -195,6 +269,9 @@ export default class Path {
195
269
  return new Path(startPoint, commands);
196
270
  }
197
271
  static fromRenderable(renderable) {
272
+ if (renderable.path) {
273
+ return renderable.path;
274
+ }
198
275
  return new Path(renderable.startPoint, renderable.commands);
199
276
  }
200
277
  toRenderable(fill) {
@@ -202,22 +279,25 @@ export default class Path {
202
279
  startPoint: this.startPoint,
203
280
  style: fill,
204
281
  commands: this.parts,
282
+ path: this,
205
283
  };
206
284
  }
207
285
  toString() {
286
+ if (this.cachedStringVersion) {
287
+ return this.cachedStringVersion;
288
+ }
208
289
  // Hueristic: Try to determine whether converting absolute to relative commands is worth it.
209
- // If we're near (0, 0), it probably isn't worth it and if bounding boxes are large,
210
- // it also probably isn't worth it.
211
- const makeRelativeCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.size.x) < 2
212
- && Math.abs(this.bbox.topLeft.y) > 10 && Math.abs(this.bbox.size.y) < 2;
213
- return Path.toString(this.startPoint, this.parts, !makeRelativeCommands);
290
+ const makeRelativeCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.topLeft.y) > 10;
291
+ const result = Path.toString(this.startPoint, this.parts, !makeRelativeCommands);
292
+ this.cachedStringVersion = result;
293
+ return result;
214
294
  }
215
295
  serialize() {
216
296
  return this.toString();
217
297
  }
218
298
  // @param onlyAbsCommands - True if we should avoid converting absolute coordinates to relative offsets -- such
219
299
  // conversions can lead to smaller output strings, but also take time.
220
- static toString(startPoint, parts, onlyAbsCommands = true) {
300
+ static toString(startPoint, parts, onlyAbsCommands) {
221
301
  const result = [];
222
302
  let prevPoint;
223
303
  const addCommand = (command, ...points) => {
@@ -452,7 +532,9 @@ export default class Path {
452
532
  lastPos = allArgs[allArgs.length - 1];
453
533
  }
454
534
  }
455
- return new Path(startPos !== null && startPos !== void 0 ? startPos : Vec2.zero, commands);
535
+ const result = new Path(startPos !== null && startPos !== void 0 ? startPos : Vec2.zero, commands);
536
+ result.cachedStringVersion = pathString;
537
+ return result;
456
538
  }
457
539
  }
458
540
  Path.empty = new Path(Vec2.zero, []);
@@ -1,6 +1,6 @@
1
1
  import LineSegment2 from './LineSegment2';
2
2
  import { Vec2 } from './Vec2';
3
- // invariant: w > 0, h > 0.
3
+ // invariant: w 0, h 0.
4
4
  export default class Rect2 {
5
5
  constructor(x, y, w, h) {
6
6
  this.x = x;
@@ -0,0 +1,11 @@
1
+ import Mat33 from './Mat33';
2
+ import Vec3 from './Vec3';
3
+ export default class Triangle {
4
+ readonly vertex1: Vec3;
5
+ readonly vertex2: Vec3;
6
+ readonly vertex3: Vec3;
7
+ constructor(vertex1: Vec3, vertex2: Vec3, vertex3: Vec3);
8
+ map(mapping: (vertex: Vec3) => Vec3): Triangle;
9
+ transformed2DBy(affineTransform: Mat33): Triangle;
10
+ transformedBy(linearTransform: Mat33): Triangle;
11
+ }
@@ -0,0 +1,19 @@
1
+ export default class Triangle {
2
+ constructor(vertex1, vertex2, vertex3) {
3
+ this.vertex1 = vertex1;
4
+ this.vertex2 = vertex2;
5
+ this.vertex3 = vertex3;
6
+ }
7
+ map(mapping) {
8
+ return new Triangle(mapping(this.vertex1), mapping(this.vertex2), mapping(this.vertex3));
9
+ }
10
+ // Transform, treating this as composed of 2D points.
11
+ transformed2DBy(affineTransform) {
12
+ return this.map(affineTransform.transformVec2);
13
+ }
14
+ // Transforms this by a linear transform --- verticies are treated as
15
+ // 3D points.
16
+ transformedBy(linearTransform) {
17
+ return this.map(linearTransform.transformVec3);
18
+ }
19
+ }
@@ -71,9 +71,9 @@ export default class Display {
71
71
  return this.dryInkRenderer.canRenderFromWithoutDataLoss(renderer);
72
72
  },
73
73
  blockResolution: cacheBlockResolution,
74
- cacheSize: 600 * 600 * 4 * 120,
74
+ cacheSize: 600 * 600 * 4 * 90,
75
75
  maxScale: 1.4,
76
- minComponentsPerCache: 45,
76
+ minComponentsPerCache: 20,
77
77
  minComponentsToUseCache: 105,
78
78
  });
79
79
  this.editor.notifier.on(EditorEventType.DisplayResized, event => {
@@ -1,7 +1,10 @@
1
1
  export interface TextRendererLocalization {
2
2
  pathNodeCount(pathCount: number): string;
3
3
  textNodeCount(nodeCount: number): string;
4
+ imageNodeCount(nodeCount: number): string;
4
5
  textNode(content: string): string;
6
+ unlabeledImageNode: string;
7
+ imageNode(label: string): string;
5
8
  rerenderAsText: string;
6
9
  }
7
10
  export declare const defaultTextRendererLocalization: TextRendererLocalization;
@@ -1,6 +1,9 @@
1
1
  export const defaultTextRendererLocalization = {
2
2
  pathNodeCount: (count) => `There are ${count} visible path objects.`,
3
3
  textNodeCount: (count) => `There are ${count} visible text nodes.`,
4
+ imageNodeCount: (nodeCount) => `There are ${nodeCount} visible image nodes.`,
4
5
  textNode: (content) => `Text: ${content}`,
6
+ imageNode: (label) => `Image: ${label}`,
7
+ unlabeledImageNode: 'Unlabeled image',
5
8
  rerenderAsText: 'Re-render as text',
6
9
  };