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.
- package/.github/ISSUE_TEMPLATE/translation.md +4 -1
- package/CHANGELOG.md +15 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +4 -1
- package/dist/src/Editor.js +117 -2
- package/dist/src/EditorImage.js +4 -1
- package/dist/src/SVGLoader.d.ts +4 -1
- package/dist/src/SVGLoader.js +78 -33
- package/dist/src/UndoRedoHistory.d.ts +1 -0
- package/dist/src/UndoRedoHistory.js +6 -0
- package/dist/src/Viewport.d.ts +1 -0
- package/dist/src/Viewport.js +12 -4
- package/dist/src/commands/lib.d.ts +2 -1
- package/dist/src/commands/lib.js +2 -1
- package/dist/src/commands/localization.d.ts +1 -0
- package/dist/src/commands/localization.js +1 -0
- package/dist/src/commands/uniteCommands.d.ts +4 -0
- package/dist/src/commands/uniteCommands.js +105 -0
- package/dist/src/components/AbstractComponent.d.ts +2 -0
- package/dist/src/components/AbstractComponent.js +41 -5
- package/dist/src/components/ImageComponent.d.ts +27 -0
- package/dist/src/components/ImageComponent.js +129 -0
- package/dist/src/components/Stroke.js +11 -6
- package/dist/src/components/builders/FreehandLineBuilder.js +7 -7
- package/dist/src/components/lib.d.ts +4 -2
- package/dist/src/components/lib.js +4 -2
- package/dist/src/components/localization.d.ts +2 -0
- package/dist/src/components/localization.js +2 -0
- package/dist/src/math/LineSegment2.d.ts +4 -0
- package/dist/src/math/LineSegment2.js +9 -0
- package/dist/src/math/Path.d.ts +5 -1
- package/dist/src/math/Path.js +89 -7
- package/dist/src/math/Rect2.js +1 -1
- package/dist/src/math/Triangle.d.ts +11 -0
- package/dist/src/math/Triangle.js +19 -0
- package/dist/src/rendering/Display.js +2 -2
- package/dist/src/rendering/localization.d.ts +3 -0
- package/dist/src/rendering/localization.js +3 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +9 -1
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +14 -12
- package/dist/src/rendering/renderers/SVGRenderer.js +71 -87
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
- package/dist/src/toolbar/HTMLToolbar.d.ts +1 -0
- package/dist/src/toolbar/HTMLToolbar.js +1 -0
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +3 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +21 -1
- package/dist/src/tools/BaseTool.d.ts +4 -1
- package/dist/src/tools/BaseTool.js +12 -0
- package/dist/src/tools/PasteHandler.d.ts +16 -0
- package/dist/src/tools/PasteHandler.js +142 -0
- package/dist/src/tools/Pen.d.ts +2 -1
- package/dist/src/tools/Pen.js +16 -0
- package/dist/src/tools/SelectionTool.d.ts +7 -1
- package/dist/src/tools/SelectionTool.js +63 -5
- package/dist/src/tools/ToolController.d.ts +1 -0
- package/dist/src/tools/ToolController.js +45 -29
- package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
- package/dist/src/tools/ToolSwitcherShortcut.js +26 -0
- package/dist/src/tools/lib.d.ts +2 -0
- package/dist/src/tools/lib.js +2 -0
- package/dist/src/tools/localization.d.ts +4 -0
- package/dist/src/tools/localization.js +4 -0
- package/dist/src/types.d.ts +21 -4
- package/dist/src/types.js +3 -0
- package/package.json +2 -2
- package/src/Editor.ts +131 -2
- package/src/EditorImage.ts +7 -1
- package/src/SVGLoader.ts +90 -36
- package/src/UndoRedoHistory.test.ts +33 -0
- package/src/UndoRedoHistory.ts +8 -0
- package/src/Viewport.ts +13 -4
- package/src/commands/lib.ts +2 -0
- package/src/commands/localization.ts +2 -0
- package/src/commands/uniteCommands.test.ts +23 -0
- package/src/commands/uniteCommands.ts +121 -0
- package/src/components/AbstractComponent.ts +55 -9
- package/src/components/ImageComponent.ts +153 -0
- package/src/components/Stroke.test.ts +5 -0
- package/src/components/Stroke.ts +13 -7
- package/src/components/builders/FreehandLineBuilder.ts +7 -7
- package/src/components/lib.ts +7 -2
- package/src/components/localization.ts +4 -0
- package/src/math/LineSegment2.test.ts +9 -0
- package/src/math/LineSegment2.ts +13 -0
- package/src/math/Path.test.ts +53 -0
- package/src/math/Path.toString.test.ts +4 -2
- package/src/math/Path.ts +109 -11
- package/src/math/Rect2.ts +1 -1
- package/src/math/Triangle.ts +29 -0
- package/src/rendering/Display.ts +2 -2
- package/src/rendering/localization.ts +6 -0
- package/src/rendering/renderers/AbstractRenderer.ts +17 -0
- package/src/rendering/renderers/CanvasRenderer.ts +10 -1
- package/src/rendering/renderers/DummyRenderer.ts +6 -1
- package/src/rendering/renderers/SVGRenderer.ts +76 -101
- package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
- package/src/toolbar/HTMLToolbar.ts +1 -1
- package/src/toolbar/types.ts +1 -1
- package/src/toolbar/widgets/BaseWidget.ts +27 -1
- package/src/tools/BaseTool.ts +17 -1
- package/src/tools/PasteHandler.ts +156 -0
- package/src/tools/Pen.ts +20 -1
- package/src/tools/SelectionTool.ts +80 -8
- package/src/tools/ToolController.ts +60 -46
- package/src/tools/ToolSwitcherShortcut.ts +34 -0
- package/src/tools/lib.ts +2 -0
- package/src/tools/localization.ts +10 -0
- 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(
|
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(
|
183
|
+
SerializableCommand.register(AbstractComponent.transformElementCommandId, (json: any, editor: Editor) => {
|
138
184
|
const elem = editor.image.lookupElement(json.id);
|
139
185
|
|
140
|
-
|
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
|
-
|
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
|
});
|
package/src/components/Stroke.ts
CHANGED
@@ -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 (
|
59
|
-
|
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 ±
|
15
|
+
// less than ±1 px from the curve.
|
16
16
|
const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 7;
|
17
|
-
const minSmoothingDist = viewport.getSizeOfPixelOnCanvas()
|
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
|
332
|
+
// If the boundaries have intersections, increasing the half vector's length could fix this.
|
333
333
|
if (boundariesIntersect()) {
|
334
|
-
halfVec = halfVec.times(
|
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) /
|
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 /
|
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;
|
package/src/components/lib.ts
CHANGED
@@ -1,12 +1,17 @@
|
|
1
1
|
export * from './builders/types';
|
2
2
|
export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
|
3
3
|
|
4
|
-
|
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
|
});
|
package/src/math/LineSegment2.ts
CHANGED
@@ -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
|
}
|
package/src/math/Path.test.ts
CHANGED
@@ -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,
|
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', '
|
53
|
+
'M100,100', 'l1,1', 'q1,1 -11-11', 'l10,10'
|
52
54
|
].join(''));
|
53
55
|
});
|
54
56
|
});
|