js-draw 0.3.1 → 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 +8 -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/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/math/LineSegment2.d.ts +2 -0
- package/dist/src/math/LineSegment2.js +3 -0
- 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/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/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 +142 -0
- package/dist/src/tools/SelectionTool.d.ts +7 -1
- package/dist/src/tools/SelectionTool.js +63 -5
- package/dist/src/tools/ToolController.js +36 -27
- package/dist/src/tools/lib.d.ts +1 -0
- package/dist/src/tools/lib.js +1 -0
- package/dist/src/tools/localization.d.ts +3 -0
- package/dist/src/tools/localization.js +3 -0
- package/dist/src/types.d.ts +13 -2
- package/dist/src/types.js +2 -0
- package/package.json +1 -1
- 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/builders/FreehandLineBuilder.ts +2 -2
- 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 +5 -0
- package/src/rendering/localization.ts +6 -0
- package/src/rendering/renderers/AbstractRenderer.ts +16 -0
- 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/tools/BaseTool.ts +9 -1
- package/src/tools/PasteHandler.ts +156 -0
- package/src/tools/SelectionTool.ts +80 -8
- package/src/tools/ToolController.ts +51 -44
- package/src/tools/lib.ts +1 -0
- package/src/tools/localization.ts +8 -0
- package/src/types.ts +16 -2
@@ -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);
|
@@ -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
|
}
|
@@ -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.
|
@@ -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)) {
|
@@ -7,7 +7,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
|
|
7
7
|
import Vec3 from '../../math/Vec3';
|
8
8
|
import Viewport from '../../Viewport';
|
9
9
|
import RenderingStyle from '../RenderingStyle';
|
10
|
-
import AbstractRenderer from './AbstractRenderer';
|
10
|
+
import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
|
11
11
|
|
12
12
|
export default class DummyRenderer extends AbstractRenderer {
|
13
13
|
// Variables that track the state of what's been rendered
|
@@ -17,6 +17,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
17
17
|
public lastPoint: Point2|null = null;
|
18
18
|
public objectNestingLevel: number = 0;
|
19
19
|
public lastText: string|null = null;
|
20
|
+
public lastImage: RenderableImage|null = null;
|
20
21
|
|
21
22
|
// List of points drawn since the last clear.
|
22
23
|
public pointBuffer: Point2[] = [];
|
@@ -44,6 +45,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
44
45
|
this.renderedPathCount = 0;
|
45
46
|
this.pointBuffer = [];
|
46
47
|
this.lastText = null;
|
48
|
+
this.lastImage = null;
|
47
49
|
|
48
50
|
// Ensure all objects finished rendering
|
49
51
|
if (this.objectNestingLevel > 0) {
|
@@ -96,6 +98,9 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
96
98
|
public drawText(text: string, _transform: Mat33, _style: TextStyle): void {
|
97
99
|
this.lastText = text;
|
98
100
|
}
|
101
|
+
public drawImage(image: RenderableImage) {
|
102
|
+
this.lastImage = image;
|
103
|
+
}
|
99
104
|
|
100
105
|
public startObject(boundingBox: Rect2, _clip: boolean) {
|
101
106
|
super.startObject(boundingBox);
|
@@ -9,7 +9,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
|
|
9
9
|
import { svgAttributesDataKey, SVGLoaderUnknownAttribute, SVGLoaderUnknownStyleAttribute, svgStyleAttributesDataKey } from '../../SVGLoader';
|
10
10
|
import Viewport from '../../Viewport';
|
11
11
|
import RenderingStyle from '../RenderingStyle';
|
12
|
-
import AbstractRenderer, { RenderablePathSpec } from './AbstractRenderer';
|
12
|
+
import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
|
13
13
|
|
14
14
|
const svgNameSpace = 'http://www.w3.org/2000/svg';
|
15
15
|
export default class SVGRenderer extends AbstractRenderer {
|
@@ -19,13 +19,18 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
19
19
|
|
20
20
|
private overwrittenAttrs: Record<string, string|null> = {};
|
21
21
|
|
22
|
-
|
22
|
+
// Renders onto `elem`. If `sanitize`, don't render potentially untrusted data.
|
23
|
+
public constructor(private elem: SVGSVGElement, viewport: Viewport, private sanitize: boolean = false) {
|
23
24
|
super(viewport);
|
24
25
|
this.clear();
|
25
26
|
}
|
26
27
|
|
27
28
|
// Sets an attribute on the root SVG element.
|
28
29
|
public setRootSVGAttribute(name: string, value: string|null) {
|
30
|
+
if (this.sanitize) {
|
31
|
+
return;
|
32
|
+
}
|
33
|
+
|
29
34
|
// Make the original value of the attribute restorable on clear
|
30
35
|
if (!(name in this.overwrittenAttrs)) {
|
31
36
|
this.overwrittenAttrs[name] = this.elem.getAttribute(name);
|
@@ -43,18 +48,21 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
43
48
|
}
|
44
49
|
|
45
50
|
public clear() {
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
this.
|
52
|
-
|
53
|
-
|
51
|
+
this.lastPathString = [];
|
52
|
+
|
53
|
+
if (!this.sanitize) {
|
54
|
+
// Restore all all attributes
|
55
|
+
for (const attrName in this.overwrittenAttrs) {
|
56
|
+
const value = this.overwrittenAttrs[attrName];
|
57
|
+
|
58
|
+
if (value) {
|
59
|
+
this.elem.setAttribute(attrName, value);
|
60
|
+
} else {
|
61
|
+
this.elem.removeAttribute(attrName);
|
62
|
+
}
|
54
63
|
}
|
64
|
+
this.overwrittenAttrs = {};
|
55
65
|
}
|
56
|
-
this.overwrittenAttrs = {};
|
57
|
-
this.lastPathString = [];
|
58
66
|
}
|
59
67
|
|
60
68
|
// Push [this.fullPath] to the SVG
|
@@ -91,26 +99,31 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
91
99
|
this.lastPathString.push(path.toString());
|
92
100
|
}
|
93
101
|
|
94
|
-
|
95
|
-
|
96
|
-
|
102
|
+
// Apply [elemTransform] to [elem].
|
103
|
+
private transformFrom(elemTransform: Mat33, elem: SVGElement) {
|
104
|
+
let transform = this.getCanvasToScreenTransform().rightMul(elemTransform);
|
97
105
|
const translation = transform.transformVec2(Vec2.zero);
|
98
106
|
transform = transform.rightMul(Mat33.translation(translation.times(-1)));
|
99
107
|
|
100
|
-
|
101
|
-
textElem.appendChild(document.createTextNode(text));
|
102
|
-
textElem.style.transform = `matrix(
|
108
|
+
elem.style.transform = `matrix(
|
103
109
|
${transform.a1}, ${transform.b1},
|
104
110
|
${transform.a2}, ${transform.b2},
|
105
111
|
${transform.a3}, ${transform.b3}
|
106
112
|
)`;
|
113
|
+
elem.setAttribute('x', `${toRoundedString(translation.x)}`);
|
114
|
+
elem.setAttribute('y', `${toRoundedString(translation.y)}`);
|
115
|
+
}
|
116
|
+
|
117
|
+
public drawText(text: string, transform: Mat33, style: TextStyle): void {
|
118
|
+
const textElem = document.createElementNS(svgNameSpace, 'text');
|
119
|
+
textElem.appendChild(document.createTextNode(text));
|
120
|
+
this.transformFrom(transform, textElem);
|
121
|
+
|
107
122
|
textElem.style.fontFamily = style.fontFamily;
|
108
123
|
textElem.style.fontVariant = style.fontVariant ?? '';
|
109
124
|
textElem.style.fontWeight = style.fontWeight ?? '';
|
110
125
|
textElem.style.fontSize = style.size + 'px';
|
111
126
|
textElem.style.fill = style.renderingStyle.fill.toHexString();
|
112
|
-
textElem.setAttribute('x', `${toRoundedString(translation.x)}`);
|
113
|
-
textElem.setAttribute('y', `${toRoundedString(translation.y)}`);
|
114
127
|
|
115
128
|
if (style.renderingStyle.stroke) {
|
116
129
|
const strokeStyle = style.renderingStyle.stroke;
|
@@ -122,6 +135,18 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
122
135
|
this.objectElems?.push(textElem);
|
123
136
|
}
|
124
137
|
|
138
|
+
public drawImage(image: RenderableImage) {
|
139
|
+
const svgImgElem = document.createElementNS(svgNameSpace, 'image');
|
140
|
+
svgImgElem.setAttribute('href', image.base64Url);
|
141
|
+
svgImgElem.setAttribute('width', image.image.getAttribute('width') ?? '');
|
142
|
+
svgImgElem.setAttribute('height', image.image.getAttribute('height') ?? '');
|
143
|
+
svgImgElem.setAttribute('aria-label', image.image.getAttribute('aria-label') ?? image.image.getAttribute('alt') ?? '');
|
144
|
+
this.transformFrom(image.transform, svgImgElem);
|
145
|
+
|
146
|
+
this.elem.appendChild(svgImgElem);
|
147
|
+
this.objectElems?.push(svgImgElem);
|
148
|
+
}
|
149
|
+
|
125
150
|
public startObject(boundingBox: Rect2) {
|
126
151
|
super.startObject(boundingBox);
|
127
152
|
|
@@ -137,7 +162,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
137
162
|
// Don't extend paths across objects
|
138
163
|
this.addPathToSVG();
|
139
164
|
|
140
|
-
if (loaderData) {
|
165
|
+
if (loaderData && !this.sanitize) {
|
141
166
|
// Restore any attributes unsupported by the app.
|
142
167
|
for (const elem of this.objectElems ?? []) {
|
143
168
|
const attrs = loaderData[svgAttributesDataKey] as SVGLoaderUnknownAttribute[]|undefined;
|
@@ -181,6 +206,10 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
181
206
|
|
182
207
|
// Renders a **copy** of the given element.
|
183
208
|
public drawSVGElem(elem: SVGElement) {
|
209
|
+
if (this.sanitize) {
|
210
|
+
return;
|
211
|
+
}
|
212
|
+
|
184
213
|
this.elem.appendChild(elem.cloneNode(true));
|
185
214
|
}
|
186
215
|
|
@@ -6,7 +6,7 @@ import Vec3 from '../../math/Vec3';
|
|
6
6
|
import Viewport from '../../Viewport';
|
7
7
|
import { TextRendererLocalization } from '../localization';
|
8
8
|
import RenderingStyle from '../RenderingStyle';
|
9
|
-
import AbstractRenderer from './AbstractRenderer';
|
9
|
+
import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
|
10
10
|
|
11
11
|
// Outputs a description of what was rendered.
|
12
12
|
|
@@ -14,6 +14,7 @@ export default class TextOnlyRenderer extends AbstractRenderer {
|
|
14
14
|
private descriptionBuilder: string[] = [];
|
15
15
|
private pathCount: number = 0;
|
16
16
|
private textNodeCount: number = 0;
|
17
|
+
private imageNodeCount: number = 0;
|
17
18
|
|
18
19
|
public constructor(viewport: Viewport, private localizationTable: TextRendererLocalization) {
|
19
20
|
super(viewport);
|
@@ -33,7 +34,8 @@ export default class TextOnlyRenderer extends AbstractRenderer {
|
|
33
34
|
public getDescription(): string {
|
34
35
|
return [
|
35
36
|
this.localizationTable.pathNodeCount(this.pathCount),
|
36
|
-
this.localizationTable.textNodeCount(this.textNodeCount),
|
37
|
+
...(this.textNodeCount > 0 ? this.localizationTable.textNodeCount(this.textNodeCount) : []),
|
38
|
+
...(this.imageNodeCount > 0 ? this.localizationTable.imageNodeCount(this.imageNodeCount) : []),
|
37
39
|
...this.descriptionBuilder
|
38
40
|
].join('\n');
|
39
41
|
}
|
@@ -55,6 +57,12 @@ export default class TextOnlyRenderer extends AbstractRenderer {
|
|
55
57
|
this.descriptionBuilder.push(this.localizationTable.textNode(text));
|
56
58
|
this.textNodeCount ++;
|
57
59
|
}
|
60
|
+
public drawImage(image: RenderableImage) {
|
61
|
+
const label = image.label ? this.localizationTable.imageNode(image.label) : this.localizationTable.unlabeledImageNode;
|
62
|
+
|
63
|
+
this.descriptionBuilder.push(label);
|
64
|
+
this.imageNodeCount ++;
|
65
|
+
}
|
58
66
|
public isTooSmallToRender(rect: Rect2): boolean {
|
59
67
|
return rect.maxDimension < 15 / this.getSizeOfCanvasPixelOnScreen();
|
60
68
|
}
|
package/src/tools/BaseTool.ts
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent, KeyUpEvent } from '../types';
|
1
|
+
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, EditorEventType, KeyPressEvent, KeyUpEvent, PasteEvent, CopyEvent } from '../types';
|
2
2
|
import ToolEnabledGroup from './ToolEnabledGroup';
|
3
3
|
|
4
4
|
export default abstract class BaseTool implements PointerEvtListener {
|
@@ -17,6 +17,14 @@ export default abstract class BaseTool implements PointerEvtListener {
|
|
17
17
|
return false;
|
18
18
|
}
|
19
19
|
|
20
|
+
public onCopy(_event: CopyEvent): boolean {
|
21
|
+
return false;
|
22
|
+
}
|
23
|
+
|
24
|
+
public onPaste(_event: PasteEvent): boolean {
|
25
|
+
return false;
|
26
|
+
}
|
27
|
+
|
20
28
|
public onKeyPress(_event: KeyPressEvent): boolean {
|
21
29
|
return false;
|
22
30
|
}
|