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
@@ -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(
|
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(
|
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
|
-
|
198
|
+
return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id);
|
162
199
|
}
|
163
|
-
|
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);
|
@@ -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
|
240
|
+
// If the boundaries have intersections, increasing the half vector's length could fix this.
|
241
241
|
if (boundariesIntersect()) {
|
242
|
-
halfVec = halfVec.times(
|
242
|
+
halfVec = halfVec.times(1.1);
|
243
243
|
}
|
244
244
|
// Each starts at startPt ± startVec
|
245
245
|
const lowerCurve = {
|
@@ -1,6 +1,8 @@
|
|
1
1
|
export * from './builders/types';
|
2
2
|
export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
|
3
|
-
|
3
|
+
export * from './AbstractComponent';
|
4
|
+
export { default as AbstractComponent } from './AbstractComponent';
|
4
5
|
import Stroke from './Stroke';
|
5
6
|
import Text from './Text';
|
6
|
-
|
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
|
-
|
3
|
+
export * from './AbstractComponent';
|
4
|
+
export { default as AbstractComponent } from './AbstractComponent';
|
4
5
|
import Stroke from './Stroke';
|
5
6
|
import Text from './Text';
|
6
|
-
|
7
|
+
import ImageComponent from './ImageComponent';
|
8
|
+
export { Stroke, Text, Text as TextComponent, Stroke as StrokeComponent, ImageComponent, };
|
@@ -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 {
|
@@ -17,6 +18,7 @@ export default class LineSegment2 {
|
|
17
18
|
intersection(other: LineSegment2): IntersectionResult | null;
|
18
19
|
intersects(other: LineSegment2): boolean;
|
19
20
|
closestPointTo(target: Point2): import("./Vec3").default;
|
21
|
+
transformedBy(affineTransfm: Mat33): LineSegment2;
|
20
22
|
toString(): string;
|
21
23
|
}
|
22
24
|
export {};
|
@@ -116,6 +116,9 @@ export default class LineSegment2 {
|
|
116
116
|
return this.p1;
|
117
117
|
}
|
118
118
|
}
|
119
|
+
transformedBy(affineTransfm) {
|
120
|
+
return new LineSegment2(affineTransfm.transformVec2(this.p1), affineTransfm.transformVec2(this.p2));
|
121
|
+
}
|
119
122
|
toString() {
|
120
123
|
return `LineSegment(${this.p1.toString()}, ${this.p2.toString()})`;
|
121
124
|
}
|
@@ -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
|
};
|
@@ -12,6 +12,12 @@ export interface RenderablePathSpec {
|
|
12
12
|
style: RenderingStyle;
|
13
13
|
path?: Path;
|
14
14
|
}
|
15
|
+
export interface RenderableImage {
|
16
|
+
transform: Mat33;
|
17
|
+
image: HTMLImageElement | HTMLCanvasElement;
|
18
|
+
base64Url: string;
|
19
|
+
label?: string;
|
20
|
+
}
|
15
21
|
export default abstract class AbstractRenderer {
|
16
22
|
private viewport;
|
17
23
|
private selfTransform;
|
@@ -27,6 +33,7 @@ export default abstract class AbstractRenderer {
|
|
27
33
|
protected abstract traceCubicBezierCurve(p1: Point2, p2: Point2, p3: Point2): void;
|
28
34
|
protected abstract traceQuadraticBezierCurve(controlPoint: Point2, endPoint: Point2): void;
|
29
35
|
abstract drawText(text: string, transform: Mat33, style: TextStyle): void;
|
36
|
+
abstract drawImage(image: RenderableImage): void;
|
30
37
|
abstract isTooSmallToRender(rect: Rect2): boolean;
|
31
38
|
setDraftMode(_draftMode: boolean): void;
|
32
39
|
protected objectLevel: number;
|
@@ -5,7 +5,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
|
|
5
5
|
import Vec3 from '../../math/Vec3';
|
6
6
|
import Viewport from '../../Viewport';
|
7
7
|
import RenderingStyle from '../RenderingStyle';
|
8
|
-
import AbstractRenderer, { RenderablePathSpec } from './AbstractRenderer';
|
8
|
+
import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
|
9
9
|
export default class CanvasRenderer extends AbstractRenderer {
|
10
10
|
private ctx;
|
11
11
|
private ignoreObjectsAboveLevel;
|
@@ -28,6 +28,7 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
28
28
|
protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
|
29
29
|
drawPath(path: RenderablePathSpec): void;
|
30
30
|
drawText(text: string, transform: Mat33, style: TextStyle): void;
|
31
|
+
drawImage(image: RenderableImage): void;
|
31
32
|
private clipLevels;
|
32
33
|
startObject(boundingBox: Rect2, clip: boolean): void;
|
33
34
|
endObject(): void;
|
@@ -125,6 +125,13 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
125
125
|
}
|
126
126
|
this.ctx.restore();
|
127
127
|
}
|
128
|
+
drawImage(image) {
|
129
|
+
this.ctx.save();
|
130
|
+
const transform = this.getCanvasToScreenTransform().rightMul(image.transform);
|
131
|
+
this.transformBy(transform);
|
132
|
+
this.ctx.drawImage(image.image, 0, 0);
|
133
|
+
this.ctx.restore();
|
134
|
+
}
|
128
135
|
startObject(boundingBox, clip) {
|
129
136
|
if (this.isTooSmallToRender(boundingBox)) {
|
130
137
|
this.ignoreObjectsAboveLevel = this.getNestingLevel();
|
@@ -5,7 +5,7 @@ import { Point2, Vec2 } from '../../math/Vec2';
|
|
5
5
|
import Vec3 from '../../math/Vec3';
|
6
6
|
import Viewport from '../../Viewport';
|
7
7
|
import RenderingStyle from '../RenderingStyle';
|
8
|
-
import AbstractRenderer from './AbstractRenderer';
|
8
|
+
import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
|
9
9
|
export default class DummyRenderer extends AbstractRenderer {
|
10
10
|
clearedCount: number;
|
11
11
|
renderedPathCount: number;
|
@@ -13,6 +13,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
13
13
|
lastPoint: Point2 | null;
|
14
14
|
objectNestingLevel: number;
|
15
15
|
lastText: string | null;
|
16
|
+
lastImage: RenderableImage | null;
|
16
17
|
pointBuffer: Point2[];
|
17
18
|
constructor(viewport: Viewport);
|
18
19
|
displaySize(): Vec2;
|
@@ -25,6 +26,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
25
26
|
protected traceQuadraticBezierCurve(controlPoint: Vec3, endPoint: Vec3): void;
|
26
27
|
drawPoints(..._points: Vec3[]): void;
|
27
28
|
drawText(text: string, _transform: Mat33, _style: TextStyle): void;
|
29
|
+
drawImage(image: RenderableImage): void;
|
28
30
|
startObject(boundingBox: Rect2, _clip: boolean): void;
|
29
31
|
endObject(): void;
|
30
32
|
isTooSmallToRender(_rect: Rect2): boolean;
|
@@ -11,6 +11,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
11
11
|
this.lastPoint = null;
|
12
12
|
this.objectNestingLevel = 0;
|
13
13
|
this.lastText = null;
|
14
|
+
this.lastImage = null;
|
14
15
|
// List of points drawn since the last clear.
|
15
16
|
this.pointBuffer = [];
|
16
17
|
}
|
@@ -30,6 +31,7 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
30
31
|
this.renderedPathCount = 0;
|
31
32
|
this.pointBuffer = [];
|
32
33
|
this.lastText = null;
|
34
|
+
this.lastImage = null;
|
33
35
|
// Ensure all objects finished rendering
|
34
36
|
if (this.objectNestingLevel > 0) {
|
35
37
|
throw new Error(`Within an object while clearing! Nesting level: ${this.objectNestingLevel}`);
|
@@ -73,6 +75,9 @@ export default class DummyRenderer extends AbstractRenderer {
|
|
73
75
|
drawText(text, _transform, _style) {
|
74
76
|
this.lastText = text;
|
75
77
|
}
|
78
|
+
drawImage(image) {
|
79
|
+
this.lastImage = image;
|
80
|
+
}
|
76
81
|
startObject(boundingBox, _clip) {
|
77
82
|
super.startObject(boundingBox);
|
78
83
|
this.objectNestingLevel += 1;
|
@@ -5,20 +5,23 @@ import Rect2 from '../../math/Rect2';
|
|
5
5
|
import { Point2, Vec2 } from '../../math/Vec2';
|
6
6
|
import Viewport from '../../Viewport';
|
7
7
|
import RenderingStyle from '../RenderingStyle';
|
8
|
-
import AbstractRenderer, { RenderablePathSpec } from './AbstractRenderer';
|
8
|
+
import AbstractRenderer, { RenderableImage, RenderablePathSpec } from './AbstractRenderer';
|
9
9
|
export default class SVGRenderer extends AbstractRenderer {
|
10
10
|
private elem;
|
11
|
+
private sanitize;
|
11
12
|
private lastPathStyle;
|
12
13
|
private lastPathString;
|
13
14
|
private objectElems;
|
14
15
|
private overwrittenAttrs;
|
15
|
-
constructor(elem: SVGSVGElement, viewport: Viewport);
|
16
|
+
constructor(elem: SVGSVGElement, viewport: Viewport, sanitize?: boolean);
|
16
17
|
setRootSVGAttribute(name: string, value: string | null): void;
|
17
18
|
displaySize(): Vec2;
|
18
19
|
clear(): void;
|
19
20
|
private addPathToSVG;
|
20
21
|
drawPath(pathSpec: RenderablePathSpec): void;
|
22
|
+
private transformFrom;
|
21
23
|
drawText(text: string, transform: Mat33, style: TextStyle): void;
|
24
|
+
drawImage(image: RenderableImage): void;
|
22
25
|
startObject(boundingBox: Rect2): void;
|
23
26
|
endObject(loaderData?: LoadSaveDataTable): void;
|
24
27
|
private unimplementedMessage;
|
@@ -6,9 +6,11 @@ import { svgAttributesDataKey, svgStyleAttributesDataKey } from '../../SVGLoader
|
|
6
6
|
import AbstractRenderer from './AbstractRenderer';
|
7
7
|
const svgNameSpace = 'http://www.w3.org/2000/svg';
|
8
8
|
export default class SVGRenderer extends AbstractRenderer {
|
9
|
-
|
9
|
+
// Renders onto `elem`. If `sanitize`, don't render potentially untrusted data.
|
10
|
+
constructor(elem, viewport, sanitize = false) {
|
10
11
|
super(viewport);
|
11
12
|
this.elem = elem;
|
13
|
+
this.sanitize = sanitize;
|
12
14
|
this.lastPathStyle = null;
|
13
15
|
this.lastPathString = [];
|
14
16
|
this.objectElems = null;
|
@@ -17,6 +19,9 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
17
19
|
}
|
18
20
|
// Sets an attribute on the root SVG element.
|
19
21
|
setRootSVGAttribute(name, value) {
|
22
|
+
if (this.sanitize) {
|
23
|
+
return;
|
24
|
+
}
|
20
25
|
// Make the original value of the attribute restorable on clear
|
21
26
|
if (!(name in this.overwrittenAttrs)) {
|
22
27
|
this.overwrittenAttrs[name] = this.elem.getAttribute(name);
|
@@ -32,18 +37,20 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
32
37
|
return Vec2.of(this.elem.clientWidth, this.elem.clientHeight);
|
33
38
|
}
|
34
39
|
clear() {
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
this.
|
40
|
-
|
41
|
-
|
42
|
-
|
40
|
+
this.lastPathString = [];
|
41
|
+
if (!this.sanitize) {
|
42
|
+
// Restore all all attributes
|
43
|
+
for (const attrName in this.overwrittenAttrs) {
|
44
|
+
const value = this.overwrittenAttrs[attrName];
|
45
|
+
if (value) {
|
46
|
+
this.elem.setAttribute(attrName, value);
|
47
|
+
}
|
48
|
+
else {
|
49
|
+
this.elem.removeAttribute(attrName);
|
50
|
+
}
|
43
51
|
}
|
52
|
+
this.overwrittenAttrs = {};
|
44
53
|
}
|
45
|
-
this.overwrittenAttrs = {};
|
46
|
-
this.lastPathString = [];
|
47
54
|
}
|
48
55
|
// Push [this.fullPath] to the SVG
|
49
56
|
addPathToSVG() {
|
@@ -74,25 +81,29 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
74
81
|
}
|
75
82
|
this.lastPathString.push(path.toString());
|
76
83
|
}
|
77
|
-
|
78
|
-
|
79
|
-
transform = this.getCanvasToScreenTransform().rightMul(
|
84
|
+
// Apply [elemTransform] to [elem].
|
85
|
+
transformFrom(elemTransform, elem) {
|
86
|
+
let transform = this.getCanvasToScreenTransform().rightMul(elemTransform);
|
80
87
|
const translation = transform.transformVec2(Vec2.zero);
|
81
88
|
transform = transform.rightMul(Mat33.translation(translation.times(-1)));
|
82
|
-
|
83
|
-
textElem.appendChild(document.createTextNode(text));
|
84
|
-
textElem.style.transform = `matrix(
|
89
|
+
elem.style.transform = `matrix(
|
85
90
|
${transform.a1}, ${transform.b1},
|
86
91
|
${transform.a2}, ${transform.b2},
|
87
92
|
${transform.a3}, ${transform.b3}
|
88
93
|
)`;
|
94
|
+
elem.setAttribute('x', `${toRoundedString(translation.x)}`);
|
95
|
+
elem.setAttribute('y', `${toRoundedString(translation.y)}`);
|
96
|
+
}
|
97
|
+
drawText(text, transform, style) {
|
98
|
+
var _a, _b, _c;
|
99
|
+
const textElem = document.createElementNS(svgNameSpace, 'text');
|
100
|
+
textElem.appendChild(document.createTextNode(text));
|
101
|
+
this.transformFrom(transform, textElem);
|
89
102
|
textElem.style.fontFamily = style.fontFamily;
|
90
103
|
textElem.style.fontVariant = (_a = style.fontVariant) !== null && _a !== void 0 ? _a : '';
|
91
104
|
textElem.style.fontWeight = (_b = style.fontWeight) !== null && _b !== void 0 ? _b : '';
|
92
105
|
textElem.style.fontSize = style.size + 'px';
|
93
106
|
textElem.style.fill = style.renderingStyle.fill.toHexString();
|
94
|
-
textElem.setAttribute('x', `${toRoundedString(translation.x)}`);
|
95
|
-
textElem.setAttribute('y', `${toRoundedString(translation.y)}`);
|
96
107
|
if (style.renderingStyle.stroke) {
|
97
108
|
const strokeStyle = style.renderingStyle.stroke;
|
98
109
|
textElem.style.stroke = strokeStyle.color.toHexString();
|
@@ -101,6 +112,17 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
101
112
|
this.elem.appendChild(textElem);
|
102
113
|
(_c = this.objectElems) === null || _c === void 0 ? void 0 : _c.push(textElem);
|
103
114
|
}
|
115
|
+
drawImage(image) {
|
116
|
+
var _a, _b, _c, _d, _e;
|
117
|
+
const svgImgElem = document.createElementNS(svgNameSpace, 'image');
|
118
|
+
svgImgElem.setAttribute('href', image.base64Url);
|
119
|
+
svgImgElem.setAttribute('width', (_a = image.image.getAttribute('width')) !== null && _a !== void 0 ? _a : '');
|
120
|
+
svgImgElem.setAttribute('height', (_b = image.image.getAttribute('height')) !== null && _b !== void 0 ? _b : '');
|
121
|
+
svgImgElem.setAttribute('aria-label', (_d = (_c = image.image.getAttribute('aria-label')) !== null && _c !== void 0 ? _c : image.image.getAttribute('alt')) !== null && _d !== void 0 ? _d : '');
|
122
|
+
this.transformFrom(image.transform, svgImgElem);
|
123
|
+
this.elem.appendChild(svgImgElem);
|
124
|
+
(_e = this.objectElems) === null || _e === void 0 ? void 0 : _e.push(svgImgElem);
|
125
|
+
}
|
104
126
|
startObject(boundingBox) {
|
105
127
|
super.startObject(boundingBox);
|
106
128
|
// Only accumulate a path within an object
|
@@ -113,7 +135,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
113
135
|
super.endObject(loaderData);
|
114
136
|
// Don't extend paths across objects
|
115
137
|
this.addPathToSVG();
|
116
|
-
if (loaderData) {
|
138
|
+
if (loaderData && !this.sanitize) {
|
117
139
|
// Restore any attributes unsupported by the app.
|
118
140
|
for (const elem of (_a = this.objectElems) !== null && _a !== void 0 ? _a : []) {
|
119
141
|
const attrs = loaderData[svgAttributesDataKey];
|
@@ -150,6 +172,9 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
150
172
|
}
|
151
173
|
// Renders a **copy** of the given element.
|
152
174
|
drawSVGElem(elem) {
|
175
|
+
if (this.sanitize) {
|
176
|
+
return;
|
177
|
+
}
|
153
178
|
this.elem.appendChild(elem.cloneNode(true));
|
154
179
|
}
|
155
180
|
isTooSmallToRender(_rect) {
|
@@ -5,12 +5,13 @@ import Vec3 from '../../math/Vec3';
|
|
5
5
|
import Viewport from '../../Viewport';
|
6
6
|
import { TextRendererLocalization } from '../localization';
|
7
7
|
import RenderingStyle from '../RenderingStyle';
|
8
|
-
import AbstractRenderer from './AbstractRenderer';
|
8
|
+
import AbstractRenderer, { RenderableImage } from './AbstractRenderer';
|
9
9
|
export default class TextOnlyRenderer extends AbstractRenderer {
|
10
10
|
private localizationTable;
|
11
11
|
private descriptionBuilder;
|
12
12
|
private pathCount;
|
13
13
|
private textNodeCount;
|
14
|
+
private imageNodeCount;
|
14
15
|
constructor(viewport: Viewport, localizationTable: TextRendererLocalization);
|
15
16
|
displaySize(): Vec3;
|
16
17
|
clear(): void;
|
@@ -22,6 +23,7 @@ export default class TextOnlyRenderer extends AbstractRenderer {
|
|
22
23
|
protected traceCubicBezierCurve(_p1: Vec3, _p2: Vec3, _p3: Vec3): void;
|
23
24
|
protected traceQuadraticBezierCurve(_controlPoint: Vec3, _endPoint: Vec3): void;
|
24
25
|
drawText(text: string, _transform: Mat33, _style: TextStyle): void;
|
26
|
+
drawImage(image: RenderableImage): void;
|
25
27
|
isTooSmallToRender(rect: Rect2): boolean;
|
26
28
|
drawPoints(..._points: Vec3[]): void;
|
27
29
|
}
|
@@ -8,6 +8,7 @@ export default class TextOnlyRenderer extends AbstractRenderer {
|
|
8
8
|
this.descriptionBuilder = [];
|
9
9
|
this.pathCount = 0;
|
10
10
|
this.textNodeCount = 0;
|
11
|
+
this.imageNodeCount = 0;
|
11
12
|
}
|
12
13
|
displaySize() {
|
13
14
|
// We don't have a graphical display, export a reasonable size.
|
@@ -21,7 +22,8 @@ export default class TextOnlyRenderer extends AbstractRenderer {
|
|
21
22
|
getDescription() {
|
22
23
|
return [
|
23
24
|
this.localizationTable.pathNodeCount(this.pathCount),
|
24
|
-
this.localizationTable.textNodeCount(this.textNodeCount),
|
25
|
+
...(this.textNodeCount > 0 ? this.localizationTable.textNodeCount(this.textNodeCount) : []),
|
26
|
+
...(this.imageNodeCount > 0 ? this.localizationTable.imageNodeCount(this.imageNodeCount) : []),
|
25
27
|
...this.descriptionBuilder
|
26
28
|
].join('\n');
|
27
29
|
}
|
@@ -42,6 +44,11 @@ export default class TextOnlyRenderer extends AbstractRenderer {
|
|
42
44
|
this.descriptionBuilder.push(this.localizationTable.textNode(text));
|
43
45
|
this.textNodeCount++;
|
44
46
|
}
|
47
|
+
drawImage(image) {
|
48
|
+
const label = image.label ? this.localizationTable.imageNode(image.label) : this.localizationTable.unlabeledImageNode;
|
49
|
+
this.descriptionBuilder.push(label);
|
50
|
+
this.imageNodeCount++;
|
51
|
+
}
|
45
52
|
isTooSmallToRender(rect) {
|
46
53
|
return rect.maxDimension < 15 / this.getSizeOfCanvasPixelOnScreen();
|
47
54
|
}
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, KeyPressEvent, KeyUpEvent } from '../types';
|
1
|
+
import { PointerEvtListener, WheelEvt, PointerEvt, EditorNotifier, KeyPressEvent, KeyUpEvent, PasteEvent, CopyEvent } from '../types';
|
2
2
|
import ToolEnabledGroup from './ToolEnabledGroup';
|
3
3
|
export default abstract class BaseTool implements PointerEvtListener {
|
4
4
|
private notifier;
|
@@ -11,6 +11,8 @@ export default abstract class BaseTool implements PointerEvtListener {
|
|
11
11
|
onGestureCancel(): void;
|
12
12
|
protected constructor(notifier: EditorNotifier, description: string);
|
13
13
|
onWheel(_event: WheelEvt): boolean;
|
14
|
+
onCopy(_event: CopyEvent): boolean;
|
15
|
+
onPaste(_event: PasteEvent): boolean;
|
14
16
|
onKeyPress(_event: KeyPressEvent): boolean;
|
15
17
|
onKeyUp(_event: KeyUpEvent): boolean;
|
16
18
|
setEnabled(enabled: boolean): void;
|