js-draw 0.3.1 → 0.4.0
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 +16 -0
- package/README.md +1 -3
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +15 -1
- package/dist/src/Editor.js +221 -78
- package/dist/src/EditorImage.js +4 -1
- package/dist/src/Pointer.d.ts +1 -1
- package/dist/src/Pointer.js +8 -3
- 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 +2 -0
- package/dist/src/Viewport.js +26 -5
- 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/builders/FreehandLineBuilder.js +2 -2
- 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/language/assertions.d.ts +1 -0
- package/dist/src/language/assertions.js +5 -0
- package/dist/src/math/LineSegment2.d.ts +2 -0
- package/dist/src/math/LineSegment2.js +3 -0
- package/dist/src/math/Mat33.d.ts +38 -2
- package/dist/src/math/Mat33.js +30 -1
- package/dist/src/math/Path.d.ts +1 -1
- package/dist/src/math/Path.js +10 -8
- package/dist/src/math/Vec3.d.ts +11 -1
- package/dist/src/math/Vec3.js +15 -0
- package/dist/src/math/rounding.d.ts +1 -0
- package/dist/src/math/rounding.js +13 -6
- 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 +7 -0
- package/dist/src/rendering/renderers/AbstractRenderer.js +2 -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 +5 -2
- package/dist/src/rendering/renderers/SVGRenderer.js +45 -20
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
- package/dist/src/toolbar/HTMLToolbar.js +5 -4
- package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
- package/dist/src/tools/BaseTool.d.ts +3 -1
- package/dist/src/tools/BaseTool.js +6 -0
- package/dist/src/tools/PasteHandler.d.ts +16 -0
- package/dist/src/tools/PasteHandler.js +144 -0
- package/dist/src/tools/Pen.js +1 -1
- package/dist/src/tools/SelectionTool/Selection.d.ts +54 -0
- package/dist/src/tools/SelectionTool/Selection.js +337 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +35 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.js +75 -0
- package/dist/src/tools/SelectionTool/SelectionTool.d.ts +31 -0
- package/dist/src/tools/SelectionTool/SelectionTool.js +276 -0
- package/dist/src/tools/SelectionTool/TransformMode.d.ts +34 -0
- package/dist/src/tools/SelectionTool/TransformMode.js +98 -0
- package/dist/src/tools/SelectionTool/types.d.ts +9 -0
- package/dist/src/tools/SelectionTool/types.js +11 -0
- package/dist/src/tools/ToolController.js +37 -28
- package/dist/src/tools/lib.d.ts +2 -1
- package/dist/src/tools/lib.js +2 -1
- package/dist/src/tools/localization.d.ts +3 -0
- package/dist/src/tools/localization.js +3 -0
- package/dist/src/types.d.ts +14 -3
- package/dist/src/types.js +2 -0
- package/package.json +1 -1
- package/src/Editor.css +1 -0
- package/src/Editor.ts +275 -109
- package/src/EditorImage.ts +7 -1
- package/src/Pointer.ts +8 -3
- package/src/SVGLoader.ts +90 -36
- package/src/UndoRedoHistory.test.ts +33 -0
- package/src/UndoRedoHistory.ts +8 -0
- package/src/Viewport.ts +30 -6
- 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 +53 -11
- package/src/components/ImageComponent.ts +149 -0
- package/src/components/Text.ts +2 -6
- package/src/components/builders/FreehandLineBuilder.ts +2 -2
- package/src/components/lib.ts +7 -2
- package/src/components/localization.ts +4 -0
- package/src/language/assertions.ts +6 -0
- package/src/math/LineSegment2.test.ts +9 -0
- package/src/math/LineSegment2.ts +5 -0
- package/src/math/Mat33.test.ts +14 -0
- package/src/math/Mat33.ts +43 -2
- package/src/math/Path.toString.test.ts +12 -1
- package/src/math/Path.ts +11 -9
- package/src/math/Vec3.ts +22 -1
- package/src/math/rounding.test.ts +30 -5
- package/src/math/rounding.ts +16 -7
- package/src/rendering/localization.ts +6 -0
- package/src/rendering/renderers/AbstractRenderer.ts +19 -2
- package/src/rendering/renderers/CanvasRenderer.ts +10 -1
- package/src/rendering/renderers/DummyRenderer.ts +6 -1
- package/src/rendering/renderers/SVGRenderer.ts +50 -21
- package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
- package/src/toolbar/HTMLToolbar.ts +5 -4
- package/src/toolbar/widgets/SelectionToolWidget.ts +1 -1
- package/src/tools/BaseTool.ts +9 -1
- package/src/tools/PasteHandler.ts +159 -0
- package/src/tools/Pen.ts +1 -1
- package/src/tools/SelectionTool/Selection.ts +455 -0
- package/src/tools/SelectionTool/SelectionHandle.ts +99 -0
- package/src/tools/SelectionTool/SelectionTool.css +22 -0
- package/src/tools/{SelectionTool.test.ts → SelectionTool/SelectionTool.test.ts} +21 -21
- package/src/tools/SelectionTool/SelectionTool.ts +335 -0
- package/src/tools/SelectionTool/TransformMode.ts +114 -0
- package/src/tools/SelectionTool/types.ts +11 -0
- package/src/tools/ToolController.ts +52 -45
- package/src/tools/lib.ts +2 -1
- package/src/tools/localization.ts +8 -0
- package/src/types.ts +17 -3
- package/dist/src/tools/SelectionTool.d.ts +0 -59
- package/dist/src/tools/SelectionTool.js +0 -589
- package/src/tools/SelectionTool.ts +0 -725
@@ -0,0 +1,149 @@
|
|
1
|
+
import LineSegment2 from '../math/LineSegment2';
|
2
|
+
import Mat33, { Mat33Array } 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 Mat33Array)),
|
145
|
+
});
|
146
|
+
}
|
147
|
+
}
|
148
|
+
|
149
|
+
AbstractComponent.registerComponent('image-component', ImageComponent.deserializeFromJSON);
|
package/src/components/Text.ts
CHANGED
@@ -1,5 +1,5 @@
|
|
1
1
|
import LineSegment2 from '../math/LineSegment2';
|
2
|
-
import Mat33 from '../math/Mat33';
|
2
|
+
import Mat33, { Mat33Array } from '../math/Mat33';
|
3
3
|
import Rect2 from '../math/Rect2';
|
4
4
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
5
5
|
import RenderingStyle, { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
|
@@ -200,11 +200,7 @@ export default class Text extends AbstractComponent {
|
|
200
200
|
throw new Error(`Unable to deserialize transform, ${json.transform}.`);
|
201
201
|
}
|
202
202
|
|
203
|
-
const transformData = json.transform as
|
204
|
-
number, number, number,
|
205
|
-
number, number, number,
|
206
|
-
number, number, number,
|
207
|
-
];
|
203
|
+
const transformData = json.transform as Mat33Array;
|
208
204
|
const transform = new Mat33(...transformData);
|
209
205
|
|
210
206
|
return new Text(textObjects, transform, style, getTextDimens);
|
@@ -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
|
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
|
|
@@ -149,6 +150,10 @@ export default class LineSegment2 {
|
|
149
150
|
}
|
150
151
|
}
|
151
152
|
|
153
|
+
public transformedBy(affineTransfm: Mat33): LineSegment2 {
|
154
|
+
return new LineSegment2(affineTransfm.transformVec2(this.p1), affineTransfm.transformVec2(this.p2));
|
155
|
+
}
|
156
|
+
|
152
157
|
public toString() {
|
153
158
|
return `LineSegment(${this.p1.toString()}, ${this.p2.toString()})`;
|
154
159
|
}
|
package/src/math/Mat33.test.ts
CHANGED
@@ -142,6 +142,20 @@ describe('Mat33 tests', () => {
|
|
142
142
|
).objEq(Vec2.unitX, fuzz);
|
143
143
|
});
|
144
144
|
|
145
|
+
it('should correctly apply a mapping to all components', () => {
|
146
|
+
expect(
|
147
|
+
new Mat33(
|
148
|
+
1, 2, 3,
|
149
|
+
4, 5, 6,
|
150
|
+
7, 8, 9,
|
151
|
+
).mapEntries(component => component - 1)
|
152
|
+
).toMatchObject(new Mat33(
|
153
|
+
0, 1, 2,
|
154
|
+
3, 4, 5,
|
155
|
+
6, 7, 8,
|
156
|
+
));
|
157
|
+
});
|
158
|
+
|
145
159
|
it('should convert CSS matrix(...) strings to matricies', () => {
|
146
160
|
// From MDN:
|
147
161
|
// ⎡ a c e ⎤
|
package/src/math/Mat33.ts
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
import { Point2, Vec2 } from './Vec2';
|
2
2
|
import Vec3 from './Vec3';
|
3
3
|
|
4
|
+
export type Mat33Array = [
|
5
|
+
number, number, number,
|
6
|
+
number, number, number,
|
7
|
+
number, number, number,
|
8
|
+
];
|
9
|
+
|
4
10
|
/**
|
5
11
|
* Represents a three dimensional linear transformation or
|
6
12
|
* a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
|
@@ -239,7 +245,7 @@ export default class Mat33 {
|
|
239
245
|
* ...
|
240
246
|
* ```
|
241
247
|
*/
|
242
|
-
public toArray():
|
248
|
+
public toArray(): Mat33Array {
|
243
249
|
return [
|
244
250
|
this.a1, this.a2, this.a3,
|
245
251
|
this.b1, this.b2, this.b3,
|
@@ -247,6 +253,27 @@ export default class Mat33 {
|
|
247
253
|
];
|
248
254
|
}
|
249
255
|
|
256
|
+
/**
|
257
|
+
* @example
|
258
|
+
* ```
|
259
|
+
* new Mat33(
|
260
|
+
* 1, 2, 3,
|
261
|
+
* 4, 5, 6,
|
262
|
+
* 7, 8, 9,
|
263
|
+
* ).mapEntries(component => component - 1);
|
264
|
+
* // → ⎡ 0, 1, 2 ⎤
|
265
|
+
* // ⎢ 3, 4, 5 ⎥
|
266
|
+
* // ⎣ 6, 7, 8 ⎦
|
267
|
+
* ```
|
268
|
+
*/
|
269
|
+
public mapEntries(mapping: (component: number)=>number): Mat33 {
|
270
|
+
return new Mat33(
|
271
|
+
mapping(this.a1), mapping(this.a2), mapping(this.a3),
|
272
|
+
mapping(this.b1), mapping(this.b2), mapping(this.b3),
|
273
|
+
mapping(this.c1), mapping(this.c2), mapping(this.c3),
|
274
|
+
);
|
275
|
+
}
|
276
|
+
|
250
277
|
/** Constructs a 3x3 translation matrix (for translating `Vec2`s) */
|
251
278
|
public static translation(amount: Vec2): Mat33 {
|
252
279
|
// When transforming Vec2s by a 3x3 matrix, we give the input
|
@@ -297,7 +324,21 @@ export default class Mat33 {
|
|
297
324
|
return result.rightMul(Mat33.translation(center.times(-1)));
|
298
325
|
}
|
299
326
|
|
300
|
-
/**
|
327
|
+
/** @see {@link !fromCSSMatrix} */
|
328
|
+
public toCSSMatrix(): string {
|
329
|
+
return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
|
330
|
+
}
|
331
|
+
|
332
|
+
/**
|
333
|
+
* Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
|
334
|
+
*
|
335
|
+
* Note that such a matrix has the form,
|
336
|
+
* ```
|
337
|
+
* ⎡ a c e ⎤
|
338
|
+
* ⎢ b d f ⎥
|
339
|
+
* ⎣ 0 0 1 ⎦
|
340
|
+
* ```
|
341
|
+
*/
|
301
342
|
public static fromCSSMatrix(cssString: string): Mat33 {
|
302
343
|
if (cssString === '' || cssString === 'none') {
|
303
344
|
return Mat33.identity;
|
@@ -38,7 +38,7 @@ describe('Path.toString', () => {
|
|
38
38
|
const path = new Path(Vec2.of(1000, 2_000_000), [
|
39
39
|
{
|
40
40
|
kind: PathCommandType.LineTo,
|
41
|
-
point: Vec2.of(30.
|
41
|
+
point: Vec2.of(30.00000001, 40.000000001),
|
42
42
|
},
|
43
43
|
]);
|
44
44
|
|
@@ -53,4 +53,15 @@ describe('Path.toString', () => {
|
|
53
53
|
'M100,100', 'l1,1', 'q1,1 -11-11', 'l10,10'
|
54
54
|
].join(''));
|
55
55
|
});
|
56
|
+
|
57
|
+
it('should not lose precision when saving', () => {
|
58
|
+
const pathStr = 'M184.2,52.3l-.2-.2q-2.7,2.4 -3.2,3.5q-2.8,7 -.9,6.1q4.3-2.6 4.8-6.1q1.2-8.8 .4-8.3q-4.2,5.2 -3.9,3.9q.2-1.6 .3-2.1q.2-1.3 -.2-1q-3.8,6.5 -3.2,3.3q.6-4.1 1.1-5.3q4.1-10 3.3-8.3q-5.3,13.1 -6.6,14.1q-3.3,2.8 -1.8-1.5q2.8-9.7 2.7-8.4q0,.3 0,.4q-1.4,7.1 -2.7,8.5q-2.6,3.2 -2.5,2.9q-.3-1.9 -.7-1.9q-4.1,4.4 -2.9,1.9q1.1-3 .3-2.6q-1.8,2 -2.5,2.4q-4.5,2.8 -4.2,1.9q.3-1.6 .2-1.4q1.5,2.2 1.3,2.9q-.8,3.9 -.5,3.3q.8-7.6 2.5-13.3q2.6-9.2 2.9-6.9q.3,1.4 .3,1.2q-.7-.4 -.9,0q-2.2,11.6 -7.6,13.6q-3.9,1.6 -2.1-1.3q3-5.5 2.6-3.4q-.2,1.8 -.5,1.8q-3.2,.5 -4.1,1.2q-2.6,2.6 -1.9,2.5q4.7-4.4 3.7-5.5q-1.1-.9 -1.6-.6q-7.2,7.5 -3.9,6.5q.3-.1 .4-.4q.6-5.3 -.2-4.9q-2.8,2.3 -3.1,2.4q-3.7,1.5 -3.5,.5q.3-3.6 1.4-3.3q3.5,.7 1.9,2.4q-1.7,2.3 -1.6,.8q0-3.5 -.9-3.1q-5.1,3.3 -4.9,2.8q.1-4 -.8-3.5q-4.3,3.4 -4.6,2.5q-1-2.1 .5-8.7l-.2,0q-1.6,6.6 -.7,8.9q.7,1.2 5.2-2.3q.4-.5 .2,3.1q.1,1 5.5-2.4q.4-.4 .3,2.7q.1,2 2.4-.4q1.7-2.3 -2.1-3.2q-1.7-.3 -2,3.7q0,1.4 4.1-.1q.3-.1 3.1-2.4q.3-.5 -.4,4.5q0-.1 -.2,0q-2.6,1.2 4.5-5.7q0-.2 .8,.6q.9,.6 -3.7,4.7q-.5,1 2.7-1.7q.6-.7 3.7-1.2q.7-.2 .9-2.2q.1-2.7 -3.4,3.2q-1.8,3.4 2.7,1.9q5.6-2.1 7.8-14q-.1,.1 .3,.4q.6,.1 .3-1.6q-.7-2.8 -3.7,6.7q-1.8,5.8 -2.5,13.5q.1,1.1 1.3-3.1q.2-1 -1.3-3.3q-.5-.5 -1,1.6q-.1,1.3 4.8-1.5q1-1 3-2q.1-.4 -1.1,2q-1.1,3.1 3.7-1.3q-.4,0 -.1,1.5q.3,.8 3.3-2.5q1.3-1.6 2.7-8.9q0-.1 0-.4q-.3-1.9 -3.5,8.2q-1.3,4.9 2.4,2.1q1.4-1.2 6.6-14.3q.8-2.4 -3.9,7.9q-.6,1.3 -1.1,5.5q-.3,3.7 4-3.1q-.2,0 -.6,.6q-.2,.6 -.3,2.3q0,1.8 4.7-3.5q.1-.5 -1.2,7.9q-.5,3.2 -4.6,5.7q-1.3,1 1.5-5.5q.4-1.1 3.01-3.5';
|
59
|
+
|
60
|
+
const path1 = Path.fromString(pathStr);
|
61
|
+
path1['cachedStringVersion'] = null; // Clear the cache.
|
62
|
+
const path = Path.fromString(path1.toString(true));
|
63
|
+
path1['cachedStringVersion'] = null; // Clear the cache.
|
64
|
+
|
65
|
+
expect(path.toString(true)).toBe(path1.toString(true));
|
66
|
+
});
|
56
67
|
});
|
package/src/math/Path.ts
CHANGED
@@ -378,15 +378,17 @@ export default class Path {
|
|
378
378
|
|
379
379
|
private cachedStringVersion: string|null = null;
|
380
380
|
|
381
|
-
public toString(): string {
|
381
|
+
public toString(useNonAbsCommands?: boolean): string {
|
382
382
|
if (this.cachedStringVersion) {
|
383
383
|
return this.cachedStringVersion;
|
384
384
|
}
|
385
385
|
|
386
|
-
|
387
|
-
|
386
|
+
if (useNonAbsCommands === undefined) {
|
387
|
+
// Hueristic: Try to determine whether converting absolute to relative commands is worth it.
|
388
|
+
useNonAbsCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.topLeft.y) > 10;
|
389
|
+
}
|
388
390
|
|
389
|
-
const result = Path.toString(this.startPoint, this.parts, !
|
391
|
+
const result = Path.toString(this.startPoint, this.parts, !useNonAbsCommands);
|
390
392
|
this.cachedStringVersion = result;
|
391
393
|
return result;
|
392
394
|
}
|
@@ -409,10 +411,13 @@ export default class Path {
|
|
409
411
|
const roundedPrevY = prevPoint ? toRoundedString(prevPoint.y) : '';
|
410
412
|
|
411
413
|
for (const point of points) {
|
414
|
+
const xComponent = toRoundedString(point.x);
|
415
|
+
const yComponent = toRoundedString(point.y);
|
416
|
+
|
412
417
|
// Relative commands are often shorter as strings than absolute commands.
|
413
418
|
if (!makeAbsCommand) {
|
414
|
-
const xComponentRelative = toStringOfSamePrecision(point.x - prevPoint!.x, roundedPrevX, roundedPrevY);
|
415
|
-
const yComponentRelative = toStringOfSamePrecision(point.y - prevPoint!.y, roundedPrevX, roundedPrevY);
|
419
|
+
const xComponentRelative = toStringOfSamePrecision(point.x - prevPoint!.x, xComponent, roundedPrevX, roundedPrevY);
|
420
|
+
const yComponentRelative = toStringOfSamePrecision(point.y - prevPoint!.y, yComponent, roundedPrevX, roundedPrevY);
|
416
421
|
|
417
422
|
// No need for an additional separator if it starts with a '-'
|
418
423
|
if (yComponentRelative.charAt(0) === '-') {
|
@@ -421,9 +426,6 @@ export default class Path {
|
|
421
426
|
relativeCommandParts.push(`${xComponentRelative},${yComponentRelative}`);
|
422
427
|
}
|
423
428
|
} else {
|
424
|
-
const xComponent = toRoundedString(point.x);
|
425
|
-
const yComponent = toRoundedString(point.y);
|
426
|
-
|
427
429
|
absoluteCommandParts.push(`${xComponent},${yComponent}`);
|
428
430
|
}
|
429
431
|
}
|
package/src/math/Vec3.ts
CHANGED
@@ -95,6 +95,27 @@ export default class Vec3 {
|
|
95
95
|
);
|
96
96
|
}
|
97
97
|
|
98
|
+
/**
|
99
|
+
* If `other` is a `Vec3`, multiplies `this` component-wise by `other`. Otherwise,
|
100
|
+
* if `other is a `number`, returns the result of scalar multiplication.
|
101
|
+
*
|
102
|
+
* @example
|
103
|
+
* ```
|
104
|
+
* Vec3.of(1, 2, 3).scale(Vec3.of(2, 4, 6)); // → Vec3(2, 8, 18)
|
105
|
+
* ```
|
106
|
+
*/
|
107
|
+
public scale(other: Vec3|number): Vec3 {
|
108
|
+
if (typeof other === 'number') {
|
109
|
+
return this.times(other);
|
110
|
+
}
|
111
|
+
|
112
|
+
return Vec3.of(
|
113
|
+
this.x * other.x,
|
114
|
+
this.y * other.y,
|
115
|
+
this.z * other.z,
|
116
|
+
);
|
117
|
+
}
|
118
|
+
|
98
119
|
/**
|
99
120
|
* Returns a vector orthogonal to this. If this is a Vec2, returns `this` rotated
|
100
121
|
* 90 degrees counter-clockwise.
|
@@ -158,7 +179,7 @@ export default class Vec3 {
|
|
158
179
|
);
|
159
180
|
}
|
160
181
|
|
161
|
-
public asArray(): number
|
182
|
+
public asArray(): [ number, number, number ] {
|
162
183
|
return [this.x, this.y, this.z];
|
163
184
|
}
|
164
185
|
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { toRoundedString, toStringOfSamePrecision } from './rounding';
|
1
|
+
import { cleanUpNumber, toRoundedString, toStringOfSamePrecision } from './rounding';
|
2
2
|
|
3
3
|
describe('toRoundedString', () => {
|
4
4
|
it('should round up numbers endings similar to .999999999999999', () => {
|
@@ -12,18 +12,28 @@ describe('toRoundedString', () => {
|
|
12
12
|
expect(toRoundedString(10.999999998)).toBe('11');
|
13
13
|
});
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
it('should round strings with multiple digits after the ending decimal points', () => {
|
16
|
+
expect(toRoundedString(292.2 - 292.8)).toBe('-.6');
|
17
|
+
expect(toRoundedString(4.06425600000023)).toBe('4.064256');
|
18
|
+
});
|
19
19
|
|
20
20
|
it('should round down strings ending endings similar to .00000001', () => {
|
21
21
|
expect(toRoundedString(10.00000001)).toBe('10');
|
22
|
+
expect(toRoundedString(-30.00000001)).toBe('-30');
|
23
|
+
expect(toRoundedString(-14.20000000000002)).toBe('-14.2');
|
24
|
+
});
|
25
|
+
|
26
|
+
it('should not round numbers insufficiently close to the next', () => {
|
27
|
+
expect(toRoundedString(-10.9999)).toBe('-10.9999');
|
28
|
+
expect(toRoundedString(-10.0001)).toBe('-10.0001');
|
29
|
+
expect(toRoundedString(-10.123499)).toBe('-10.123499');
|
30
|
+
expect(toRoundedString(0.00123499)).toBe('.00123499');
|
22
31
|
});
|
23
32
|
});
|
24
33
|
|
25
34
|
it('toStringOfSamePrecision', () => {
|
26
35
|
expect(toStringOfSamePrecision(1.23456, '1.12')).toBe('1.23');
|
36
|
+
expect(toStringOfSamePrecision(1.23456, '1.120')).toBe('1.235');
|
27
37
|
expect(toStringOfSamePrecision(1.23456, '1.1')).toBe('1.2');
|
28
38
|
expect(toStringOfSamePrecision(1.23456, '1.1', '5.32')).toBe('1.23');
|
29
39
|
expect(toStringOfSamePrecision(-1.23456, '1.1', '5.32')).toBe('-1.23');
|
@@ -37,4 +47,19 @@ it('toStringOfSamePrecision', () => {
|
|
37
47
|
expect(toStringOfSamePrecision(-0.09999999999999432, '291.3')).toBe('-.1');
|
38
48
|
expect(toStringOfSamePrecision(-0.9999999999999432, '291.3')).toBe('-1');
|
39
49
|
expect(toStringOfSamePrecision(9998.9, '.1', '-11')).toBe('9998.9');
|
50
|
+
expect(toStringOfSamePrecision(-14.20000000000002, '.000001', '-11')).toBe('-14.2');
|
51
|
+
});
|
52
|
+
|
53
|
+
it('cleanUpNumber', () => {
|
54
|
+
expect(cleanUpNumber('000.0000')).toBe('0');
|
55
|
+
expect(cleanUpNumber('-000.0000')).toBe('0');
|
56
|
+
expect(cleanUpNumber('0.0000')).toBe('0');
|
57
|
+
expect(cleanUpNumber('0.001')).toBe('.001');
|
58
|
+
expect(cleanUpNumber('-0.001')).toBe('-.001');
|
59
|
+
expect(cleanUpNumber('-0.000000001')).toBe('-.000000001');
|
60
|
+
expect(cleanUpNumber('-0.00000000100')).toBe('-.000000001');
|
61
|
+
expect(cleanUpNumber('1234')).toBe('1234');
|
62
|
+
expect(cleanUpNumber('1234.5')).toBe('1234.5');
|
63
|
+
expect(cleanUpNumber('1234.500')).toBe('1234.5');
|
64
|
+
expect(cleanUpNumber('1.1368683772161603e-13')).toBe('0');
|
40
65
|
});
|
package/src/math/rounding.ts
CHANGED
@@ -1,9 +1,17 @@
|
|
1
1
|
// @packageDocumentation @internal
|
2
2
|
|
3
3
|
// Clean up stringified numbers
|
4
|
-
const cleanUpNumber = (text: string) => {
|
4
|
+
export const cleanUpNumber = (text: string) => {
|
5
5
|
// Regular expression substitions can be somewhat expensive. Only do them
|
6
6
|
// if necessary.
|
7
|
+
|
8
|
+
if (text.indexOf('e') > 0) {
|
9
|
+
// Round to zero.
|
10
|
+
if (text.match(/[eE][-]\d{2,}$/)) {
|
11
|
+
return '0';
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
7
15
|
const lastChar = text.charAt(text.length - 1);
|
8
16
|
if (lastChar === '0' || lastChar === '.') {
|
9
17
|
// Remove trailing zeroes
|
@@ -12,10 +20,6 @@ const cleanUpNumber = (text: string) => {
|
|
12
20
|
|
13
21
|
// Remove trailing period
|
14
22
|
text = text.replace(/[.]$/, '');
|
15
|
-
|
16
|
-
if (text === '-0') {
|
17
|
-
return '0';
|
18
|
-
}
|
19
23
|
}
|
20
24
|
|
21
25
|
const firstChar = text.charAt(0);
|
@@ -23,6 +27,11 @@ const cleanUpNumber = (text: string) => {
|
|
23
27
|
// Remove unnecessary leading zeroes.
|
24
28
|
text = text.replace(/^(0+)[.]/, '.');
|
25
29
|
text = text.replace(/^-(0+)[.]/, '-.');
|
30
|
+
text = text.replace(/^(-?)0+$/, '$10');
|
31
|
+
}
|
32
|
+
|
33
|
+
if (text === '-0') {
|
34
|
+
return '0';
|
26
35
|
}
|
27
36
|
|
28
37
|
return text;
|
@@ -31,8 +40,8 @@ const cleanUpNumber = (text: string) => {
|
|
31
40
|
export const toRoundedString = (num: number): string => {
|
32
41
|
// Try to remove rounding errors. If the number ends in at least three/four zeroes
|
33
42
|
// (or nines) just one or two digits, it's probably a rounding error.
|
34
|
-
const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,
|
35
|
-
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,
|
43
|
+
const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,4}$/;
|
44
|
+
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,4}$/;
|
36
45
|
|
37
46
|
let text = num.toString(10);
|
38
47
|
if (text.indexOf('.') === -1) {
|
@@ -2,13 +2,19 @@
|
|
2
2
|
export interface TextRendererLocalization {
|
3
3
|
pathNodeCount(pathCount: number): string;
|
4
4
|
textNodeCount(nodeCount: number): string;
|
5
|
+
imageNodeCount(nodeCount: number): string;
|
5
6
|
textNode(content: string): string;
|
7
|
+
unlabeledImageNode: string;
|
8
|
+
imageNode(label: string): string;
|
6
9
|
rerenderAsText: string;
|
7
10
|
}
|
8
11
|
|
9
12
|
export const defaultTextRendererLocalization: TextRendererLocalization = {
|
10
13
|
pathNodeCount: (count: number) => `There are ${count} visible path objects.`,
|
11
14
|
textNodeCount: (count: number) => `There are ${count} visible text nodes.`,
|
15
|
+
imageNodeCount: (nodeCount: number) => `There are ${nodeCount} visible image nodes.`,
|
12
16
|
textNode: (content: string) => `Text: ${content}`,
|
17
|
+
imageNode: (label: string) => `Image: ${label}`,
|
18
|
+
unlabeledImageNode: 'Unlabeled image',
|
13
19
|
rerenderAsText: 'Re-render as text',
|
14
20
|
};
|
@@ -14,6 +14,21 @@ export interface RenderablePathSpec {
|
|
14
14
|
path?: Path;
|
15
15
|
}
|
16
16
|
|
17
|
+
export interface RenderableImage {
|
18
|
+
transform: Mat33;
|
19
|
+
|
20
|
+
// An Image or HTMLCanvasElement. If an Image, it must be loaded from the same origin as this
|
21
|
+
// (and should have `src=this.base64Url`).
|
22
|
+
image: HTMLImageElement|HTMLCanvasElement;
|
23
|
+
|
24
|
+
// All images that can be drawn **must** have a base64 URL in the form
|
25
|
+
// data:image/[format];base64,[data here]
|
26
|
+
// If `image` is an Image, this should be equivalent to `image.src`.
|
27
|
+
base64Url: string;
|
28
|
+
|
29
|
+
label?: string;
|
30
|
+
}
|
31
|
+
|
17
32
|
export default abstract class AbstractRenderer {
|
18
33
|
// If null, this' transformation is linked to the Viewport
|
19
34
|
private selfTransform: Mat33|null = null;
|
@@ -41,6 +56,7 @@ export default abstract class AbstractRenderer {
|
|
41
56
|
controlPoint: Point2, endPoint: Point2,
|
42
57
|
): void;
|
43
58
|
public abstract drawText(text: string, transform: Mat33, style: TextStyle): void;
|
59
|
+
public abstract drawImage(image: RenderableImage): void;
|
44
60
|
|
45
61
|
// Returns true iff the given rectangle is so small, rendering anything within
|
46
62
|
// it has no effect on the image.
|
@@ -106,8 +122,9 @@ export default abstract class AbstractRenderer {
|
|
106
122
|
}
|
107
123
|
}
|
108
124
|
|
109
|
-
// Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill]
|
110
|
-
|
125
|
+
// Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill].
|
126
|
+
// This is equivalent to `drawPath(Path.fromRect(...).toRenderable(...))`.
|
127
|
+
public drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle) {
|
111
128
|
const path = Path.fromRect(rect, lineWidth);
|
112
129
|
this.drawPath(path.toRenderable(lineFill));
|
113
130
|
}
|
@@ -6,7 +6,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
|
|
6
6
|
import Vec3 from '../../math/Vec3';
|
7
7
|
import Viewport from '../../Viewport';
|
8
8
|
import RenderingStyle from '../RenderingStyle';
|
9
|
-
import AbstractRenderer, { RenderablePathSpec } from './AbstractRenderer';
|
9
|
+
import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
|
10
10
|
|
11
11
|
export default class CanvasRenderer extends AbstractRenderer {
|
12
12
|
private ignoreObjectsAboveLevel: number|null = null;
|
@@ -168,6 +168,15 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
168
168
|
this.ctx.restore();
|
169
169
|
}
|
170
170
|
|
171
|
+
public drawImage(image: RenderableImage) {
|
172
|
+
this.ctx.save();
|
173
|
+
const transform = this.getCanvasToScreenTransform().rightMul(image.transform);
|
174
|
+
this.transformBy(transform);
|
175
|
+
|
176
|
+
this.ctx.drawImage(image.image, 0, 0);
|
177
|
+
this.ctx.restore();
|
178
|
+
}
|
179
|
+
|
171
180
|
private clipLevels: number[] = [];
|
172
181
|
public startObject(boundingBox: Rect2, clip: boolean) {
|
173
182
|
if (this.isTooSmallToRender(boundingBox)) {
|