js-draw 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/translation.md +4 -1
- package/CHANGELOG.md +15 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +4 -1
- package/dist/src/Editor.js +117 -2
- package/dist/src/EditorImage.js +4 -1
- package/dist/src/SVGLoader.d.ts +4 -1
- package/dist/src/SVGLoader.js +78 -33
- package/dist/src/UndoRedoHistory.d.ts +1 -0
- package/dist/src/UndoRedoHistory.js +6 -0
- package/dist/src/Viewport.d.ts +1 -0
- package/dist/src/Viewport.js +12 -4
- package/dist/src/commands/lib.d.ts +2 -1
- package/dist/src/commands/lib.js +2 -1
- package/dist/src/commands/localization.d.ts +1 -0
- package/dist/src/commands/localization.js +1 -0
- package/dist/src/commands/uniteCommands.d.ts +4 -0
- package/dist/src/commands/uniteCommands.js +105 -0
- package/dist/src/components/AbstractComponent.d.ts +2 -0
- package/dist/src/components/AbstractComponent.js +41 -5
- package/dist/src/components/ImageComponent.d.ts +27 -0
- package/dist/src/components/ImageComponent.js +129 -0
- package/dist/src/components/Stroke.js +11 -6
- package/dist/src/components/builders/FreehandLineBuilder.js +7 -7
- package/dist/src/components/lib.d.ts +4 -2
- package/dist/src/components/lib.js +4 -2
- package/dist/src/components/localization.d.ts +2 -0
- package/dist/src/components/localization.js +2 -0
- package/dist/src/math/LineSegment2.d.ts +4 -0
- package/dist/src/math/LineSegment2.js +9 -0
- package/dist/src/math/Path.d.ts +5 -1
- package/dist/src/math/Path.js +89 -7
- package/dist/src/math/Rect2.js +1 -1
- package/dist/src/math/Triangle.d.ts +11 -0
- package/dist/src/math/Triangle.js +19 -0
- package/dist/src/rendering/Display.js +2 -2
- package/dist/src/rendering/localization.d.ts +3 -0
- package/dist/src/rendering/localization.js +3 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +9 -1
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/CanvasRenderer.js +7 -0
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/DummyRenderer.js +5 -0
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +14 -12
- package/dist/src/rendering/renderers/SVGRenderer.js +71 -87
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +3 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +8 -1
- package/dist/src/toolbar/HTMLToolbar.d.ts +1 -0
- package/dist/src/toolbar/HTMLToolbar.js +1 -0
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +3 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +21 -1
- package/dist/src/tools/BaseTool.d.ts +4 -1
- package/dist/src/tools/BaseTool.js +12 -0
- package/dist/src/tools/PasteHandler.d.ts +16 -0
- package/dist/src/tools/PasteHandler.js +142 -0
- package/dist/src/tools/Pen.d.ts +2 -1
- package/dist/src/tools/Pen.js +16 -0
- package/dist/src/tools/SelectionTool.d.ts +7 -1
- package/dist/src/tools/SelectionTool.js +63 -5
- package/dist/src/tools/ToolController.d.ts +1 -0
- package/dist/src/tools/ToolController.js +45 -29
- package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
- package/dist/src/tools/ToolSwitcherShortcut.js +26 -0
- package/dist/src/tools/lib.d.ts +2 -0
- package/dist/src/tools/lib.js +2 -0
- package/dist/src/tools/localization.d.ts +4 -0
- package/dist/src/tools/localization.js +4 -0
- package/dist/src/types.d.ts +21 -4
- package/dist/src/types.js +3 -0
- package/package.json +2 -2
- package/src/Editor.ts +131 -2
- package/src/EditorImage.ts +7 -1
- package/src/SVGLoader.ts +90 -36
- package/src/UndoRedoHistory.test.ts +33 -0
- package/src/UndoRedoHistory.ts +8 -0
- package/src/Viewport.ts +13 -4
- package/src/commands/lib.ts +2 -0
- package/src/commands/localization.ts +2 -0
- package/src/commands/uniteCommands.test.ts +23 -0
- package/src/commands/uniteCommands.ts +121 -0
- package/src/components/AbstractComponent.ts +55 -9
- package/src/components/ImageComponent.ts +153 -0
- package/src/components/Stroke.test.ts +5 -0
- package/src/components/Stroke.ts +13 -7
- package/src/components/builders/FreehandLineBuilder.ts +7 -7
- package/src/components/lib.ts +7 -2
- package/src/components/localization.ts +4 -0
- package/src/math/LineSegment2.test.ts +9 -0
- package/src/math/LineSegment2.ts +13 -0
- package/src/math/Path.test.ts +53 -0
- package/src/math/Path.toString.test.ts +4 -2
- package/src/math/Path.ts +109 -11
- package/src/math/Rect2.ts +1 -1
- package/src/math/Triangle.ts +29 -0
- package/src/rendering/Display.ts +2 -2
- package/src/rendering/localization.ts +6 -0
- package/src/rendering/renderers/AbstractRenderer.ts +17 -0
- package/src/rendering/renderers/CanvasRenderer.ts +10 -1
- package/src/rendering/renderers/DummyRenderer.ts +6 -1
- package/src/rendering/renderers/SVGRenderer.ts +76 -101
- package/src/rendering/renderers/TextOnlyRenderer.ts +10 -2
- package/src/toolbar/HTMLToolbar.ts +1 -1
- package/src/toolbar/types.ts +1 -1
- package/src/toolbar/widgets/BaseWidget.ts +27 -1
- package/src/tools/BaseTool.ts +17 -1
- package/src/tools/PasteHandler.ts +156 -0
- package/src/tools/Pen.ts +20 -1
- package/src/tools/SelectionTool.ts +80 -8
- package/src/tools/ToolController.ts +60 -46
- package/src/tools/ToolSwitcherShortcut.ts +34 -0
- package/src/tools/lib.ts +2 -0
- package/src/tools/localization.ts +10 -0
- package/src/types.ts +29 -3
package/src/math/Path.ts
CHANGED
@@ -47,12 +47,9 @@ interface IntersectionResult {
|
|
47
47
|
|
48
48
|
type GeometryArrayType = Array<LineSegment2|Bezier>;
|
49
49
|
export default class Path {
|
50
|
-
private cachedGeometry: GeometryArrayType|null;
|
51
50
|
public readonly bbox: Rect2;
|
52
51
|
|
53
52
|
public constructor(public readonly startPoint: Point2, public readonly parts: PathCommand[]) {
|
54
|
-
this.cachedGeometry = null;
|
55
|
-
|
56
53
|
// Initial bounding box contains one point: the start point.
|
57
54
|
this.bbox = Rect2.bboxOf([startPoint]);
|
58
55
|
|
@@ -63,6 +60,8 @@ export default class Path {
|
|
63
60
|
}
|
64
61
|
}
|
65
62
|
|
63
|
+
private cachedGeometry: GeometryArrayType|null = null;
|
64
|
+
|
66
65
|
// Lazy-loads and returns this path's geometry
|
67
66
|
public get geometry(): Array<LineSegment2|Bezier> {
|
68
67
|
if (this.cachedGeometry) {
|
@@ -106,6 +105,41 @@ export default class Path {
|
|
106
105
|
return this.cachedGeometry;
|
107
106
|
}
|
108
107
|
|
108
|
+
private cachedPolylineApproximation: LineSegment2[]|null = null;
|
109
|
+
|
110
|
+
// Approximates this path with a group of line segments.
|
111
|
+
public polylineApproximation(): LineSegment2[] {
|
112
|
+
if (this.cachedPolylineApproximation) {
|
113
|
+
return this.cachedPolylineApproximation;
|
114
|
+
}
|
115
|
+
|
116
|
+
const points: Point2[] = [];
|
117
|
+
|
118
|
+
for (const part of this.parts) {
|
119
|
+
switch (part.kind) {
|
120
|
+
case PathCommandType.CubicBezierTo:
|
121
|
+
points.push(part.controlPoint1, part.controlPoint2, part.endPoint);
|
122
|
+
break;
|
123
|
+
case PathCommandType.QuadraticBezierTo:
|
124
|
+
points.push(part.controlPoint, part.endPoint);
|
125
|
+
break;
|
126
|
+
case PathCommandType.MoveTo:
|
127
|
+
case PathCommandType.LineTo:
|
128
|
+
points.push(part.point);
|
129
|
+
break;
|
130
|
+
}
|
131
|
+
}
|
132
|
+
|
133
|
+
const result: LineSegment2[] = [];
|
134
|
+
let prevPoint = this.startPoint;
|
135
|
+
for (const point of points) {
|
136
|
+
result.push(new LineSegment2(prevPoint, point));
|
137
|
+
prevPoint = point;
|
138
|
+
}
|
139
|
+
|
140
|
+
return result;
|
141
|
+
}
|
142
|
+
|
109
143
|
public static computeBBoxForSegment(startPoint: Point2, part: PathCommand): Rect2 {
|
110
144
|
const points = [startPoint];
|
111
145
|
let exhaustivenessCheck: never;
|
@@ -129,6 +163,10 @@ export default class Path {
|
|
129
163
|
}
|
130
164
|
|
131
165
|
public intersection(line: LineSegment2): IntersectionResult[] {
|
166
|
+
if (!line.bbox.intersects(this.bbox)) {
|
167
|
+
return [];
|
168
|
+
}
|
169
|
+
|
132
170
|
const result: IntersectionResult[] = [];
|
133
171
|
for (const part of this.geometry) {
|
134
172
|
if (part instanceof LineSegment2) {
|
@@ -229,6 +267,55 @@ export default class Path {
|
|
229
267
|
]);
|
230
268
|
}
|
231
269
|
|
270
|
+
// Treats this as a closed path and returns true if part of `rect` is roughly within
|
271
|
+
// this path's interior.
|
272
|
+
//
|
273
|
+
// Note: Assumes that this is a closed, non-self-intersecting path.
|
274
|
+
public closedRoughlyIntersects(rect: Rect2): boolean {
|
275
|
+
if (rect.containsRect(this.bbox)) {
|
276
|
+
return true;
|
277
|
+
}
|
278
|
+
|
279
|
+
// Choose a point outside of the path.
|
280
|
+
const startPt = this.bbox.topLeft.minus(Vec2.of(1, 1));
|
281
|
+
const testPts = rect.corners;
|
282
|
+
const polygon = this.polylineApproximation();
|
283
|
+
|
284
|
+
for (const point of testPts) {
|
285
|
+
const testLine = new LineSegment2(point, startPt);
|
286
|
+
|
287
|
+
let intersectionCount = 0;
|
288
|
+
for (const line of polygon) {
|
289
|
+
if (line.intersects(testLine)) {
|
290
|
+
intersectionCount ++;
|
291
|
+
}
|
292
|
+
}
|
293
|
+
|
294
|
+
// Odd? The point is within the polygon!
|
295
|
+
if (intersectionCount % 2 === 1) {
|
296
|
+
return true;
|
297
|
+
}
|
298
|
+
}
|
299
|
+
|
300
|
+
// Grow the rectangle for possible additional precision.
|
301
|
+
const grownRect = rect.grownBy(Math.min(rect.size.x, rect.size.y));
|
302
|
+
const edges = [];
|
303
|
+
for (const subrect of grownRect.divideIntoGrid(4, 4)) {
|
304
|
+
edges.push(...subrect.getEdges());
|
305
|
+
}
|
306
|
+
|
307
|
+
for (const edge of edges) {
|
308
|
+
for (const line of polygon) {
|
309
|
+
if (edge.intersects(line)) {
|
310
|
+
return true;
|
311
|
+
}
|
312
|
+
}
|
313
|
+
}
|
314
|
+
|
315
|
+
// Even? Probably no intersection.
|
316
|
+
return false;
|
317
|
+
}
|
318
|
+
|
232
319
|
// Returns a path that outlines [rect]. If [lineWidth] is not given, the resultant path is
|
233
320
|
// the outline of [rect]. Otherwise, the resultant path represents a line of width [lineWidth]
|
234
321
|
// that traces [rect].
|
@@ -273,6 +360,10 @@ export default class Path {
|
|
273
360
|
}
|
274
361
|
|
275
362
|
public static fromRenderable(renderable: RenderablePathSpec): Path {
|
363
|
+
if (renderable.path) {
|
364
|
+
return renderable.path;
|
365
|
+
}
|
366
|
+
|
276
367
|
return new Path(renderable.startPoint, renderable.commands);
|
277
368
|
}
|
278
369
|
|
@@ -281,18 +372,23 @@ export default class Path {
|
|
281
372
|
startPoint: this.startPoint,
|
282
373
|
style: fill,
|
283
374
|
commands: this.parts,
|
375
|
+
path: this,
|
284
376
|
};
|
285
377
|
}
|
286
378
|
|
379
|
+
private cachedStringVersion: string|null = null;
|
380
|
+
|
287
381
|
public toString(): string {
|
382
|
+
if (this.cachedStringVersion) {
|
383
|
+
return this.cachedStringVersion;
|
384
|
+
}
|
385
|
+
|
288
386
|
// Hueristic: Try to determine whether converting absolute to relative commands is worth it.
|
289
|
-
|
290
|
-
// it also probably isn't worth it.
|
291
|
-
const makeRelativeCommands =
|
292
|
-
Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.size.x) < 2
|
293
|
-
&& Math.abs(this.bbox.topLeft.y) > 10 && Math.abs(this.bbox.size.y) < 2;
|
387
|
+
const makeRelativeCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.topLeft.y) > 10;
|
294
388
|
|
295
|
-
|
389
|
+
const result = Path.toString(this.startPoint, this.parts, !makeRelativeCommands);
|
390
|
+
this.cachedStringVersion = result;
|
391
|
+
return result;
|
296
392
|
}
|
297
393
|
|
298
394
|
public serialize(): string {
|
@@ -301,7 +397,7 @@ export default class Path {
|
|
301
397
|
|
302
398
|
// @param onlyAbsCommands - True if we should avoid converting absolute coordinates to relative offsets -- such
|
303
399
|
// conversions can lead to smaller output strings, but also take time.
|
304
|
-
public static toString(startPoint: Point2, parts: PathCommand[], onlyAbsCommands
|
400
|
+
public static toString(startPoint: Point2, parts: PathCommand[], onlyAbsCommands?: boolean): string {
|
305
401
|
const result: string[] = [];
|
306
402
|
|
307
403
|
let prevPoint: Point2|undefined;
|
@@ -561,7 +657,9 @@ export default class Path {
|
|
561
657
|
}
|
562
658
|
}
|
563
659
|
|
564
|
-
|
660
|
+
const result = new Path(startPos ?? Vec2.zero, commands);
|
661
|
+
result.cachedStringVersion = pathString;
|
662
|
+
return result;
|
565
663
|
}
|
566
664
|
|
567
665
|
public static empty: Path = new Path(Vec2.zero, []);
|
package/src/math/Rect2.ts
CHANGED
@@ -0,0 +1,29 @@
|
|
1
|
+
import Mat33 from './Mat33';
|
2
|
+
import Vec3 from './Vec3';
|
3
|
+
|
4
|
+
export default class Triangle {
|
5
|
+
public constructor(
|
6
|
+
public readonly vertex1: Vec3,
|
7
|
+
public readonly vertex2: Vec3,
|
8
|
+
public readonly vertex3: Vec3,
|
9
|
+
) {}
|
10
|
+
|
11
|
+
public map(mapping: (vertex: Vec3)=>Vec3): Triangle {
|
12
|
+
return new Triangle(
|
13
|
+
mapping(this.vertex1),
|
14
|
+
mapping(this.vertex2),
|
15
|
+
mapping(this.vertex3),
|
16
|
+
);
|
17
|
+
}
|
18
|
+
|
19
|
+
// Transform, treating this as composed of 2D points.
|
20
|
+
public transformed2DBy(affineTransform: Mat33) {
|
21
|
+
return this.map(affineTransform.transformVec2);
|
22
|
+
}
|
23
|
+
|
24
|
+
// Transforms this by a linear transform --- verticies are treated as
|
25
|
+
// 3D points.
|
26
|
+
public transformedBy(linearTransform: Mat33) {
|
27
|
+
return this.map(linearTransform.transformVec3);
|
28
|
+
}
|
29
|
+
}
|
package/src/rendering/Display.ts
CHANGED
@@ -76,9 +76,9 @@ export default class Display {
|
|
76
76
|
return this.dryInkRenderer.canRenderFromWithoutDataLoss(renderer);
|
77
77
|
},
|
78
78
|
blockResolution: cacheBlockResolution,
|
79
|
-
cacheSize: 600 * 600 * 4 *
|
79
|
+
cacheSize: 600 * 600 * 4 * 90,
|
80
80
|
maxScale: 1.4,
|
81
|
-
minComponentsPerCache:
|
81
|
+
minComponentsPerCache: 20,
|
82
82
|
minComponentsToUseCache: 105,
|
83
83
|
});
|
84
84
|
|
@@ -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
|
};
|
@@ -11,6 +11,22 @@ export interface RenderablePathSpec {
|
|
11
11
|
startPoint: Point2;
|
12
12
|
commands: PathCommand[];
|
13
13
|
style: RenderingStyle;
|
14
|
+
path?: Path;
|
15
|
+
}
|
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;
|
14
30
|
}
|
15
31
|
|
16
32
|
export default abstract class AbstractRenderer {
|
@@ -40,6 +56,7 @@ export default abstract class AbstractRenderer {
|
|
40
56
|
controlPoint: Point2, endPoint: Point2,
|
41
57
|
): void;
|
42
58
|
public abstract drawText(text: string, transform: Mat33, style: TextStyle): void;
|
59
|
+
public abstract drawImage(image: RenderableImage): void;
|
43
60
|
|
44
61
|
// Returns true iff the given rectangle is so small, rendering anything within
|
45
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);
|
@@ -2,34 +2,35 @@
|
|
2
2
|
import { LoadSaveDataTable } from '../../components/AbstractComponent';
|
3
3
|
import { TextStyle } from '../../components/Text';
|
4
4
|
import Mat33 from '../../math/Mat33';
|
5
|
-
import Path
|
5
|
+
import Path from '../../math/Path';
|
6
6
|
import Rect2 from '../../math/Rect2';
|
7
7
|
import { toRoundedString } from '../../math/rounding';
|
8
8
|
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 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 {
|
16
|
-
private
|
17
|
-
private
|
18
|
-
|
19
|
-
private lastPathStyle: RenderingStyle|null;
|
20
|
-
private lastPath: PathCommand[]|null;
|
21
|
-
private lastPathStart: Point2|null;
|
16
|
+
private lastPathStyle: RenderingStyle|null = null;
|
17
|
+
private lastPathString: string[] = [];
|
22
18
|
private objectElems: SVGElement[]|null = null;
|
23
19
|
|
24
20
|
private overwrittenAttrs: Record<string, string|null> = {};
|
25
21
|
|
26
|
-
|
22
|
+
// Renders onto `elem`. If `sanitize`, don't render potentially untrusted data.
|
23
|
+
public constructor(private elem: SVGSVGElement, viewport: Viewport, private sanitize: boolean = false) {
|
27
24
|
super(viewport);
|
28
25
|
this.clear();
|
29
26
|
}
|
30
27
|
|
31
28
|
// Sets an attribute on the root SVG element.
|
32
29
|
public setRootSVGAttribute(name: string, value: string|null) {
|
30
|
+
if (this.sanitize) {
|
31
|
+
return;
|
32
|
+
}
|
33
|
+
|
33
34
|
// Make the original value of the attribute restorable on clear
|
34
35
|
if (!(name in this.overwrittenAttrs)) {
|
35
36
|
this.overwrittenAttrs[name] = this.elem.getAttribute(name);
|
@@ -47,57 +48,31 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
47
48
|
}
|
48
49
|
|
49
50
|
public clear() {
|
50
|
-
|
51
|
-
for (const attrName in this.overwrittenAttrs) {
|
52
|
-
const value = this.overwrittenAttrs[attrName];
|
53
|
-
|
54
|
-
if (value) {
|
55
|
-
this.elem.setAttribute(attrName, value);
|
56
|
-
} else {
|
57
|
-
this.elem.removeAttribute(attrName);
|
58
|
-
}
|
59
|
-
}
|
60
|
-
this.overwrittenAttrs = {};
|
61
|
-
}
|
51
|
+
this.lastPathString = [];
|
62
52
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
}
|
68
|
-
|
69
|
-
protected endPath(style: RenderingStyle) {
|
70
|
-
if (this.currentPath == null) {
|
71
|
-
throw new Error('No path exists to end! Make sure beginPath was called!');
|
72
|
-
}
|
73
|
-
|
74
|
-
// Try to extend the previous path, if possible
|
75
|
-
if (style.fill.eq(this.lastPathStyle?.fill) && this.lastPath != null) {
|
76
|
-
this.lastPath.push({
|
77
|
-
kind: PathCommandType.MoveTo,
|
78
|
-
point: this.pathStart!,
|
79
|
-
}, ...this.currentPath);
|
80
|
-
this.pathStart = null;
|
81
|
-
this.currentPath = null;
|
82
|
-
} else {
|
83
|
-
this.addPathToSVG();
|
84
|
-
this.lastPathStart = this.pathStart;
|
85
|
-
this.lastPathStyle = style;
|
86
|
-
this.lastPath = this.currentPath;
|
53
|
+
if (!this.sanitize) {
|
54
|
+
// Restore all all attributes
|
55
|
+
for (const attrName in this.overwrittenAttrs) {
|
56
|
+
const value = this.overwrittenAttrs[attrName];
|
87
57
|
|
88
|
-
|
89
|
-
|
58
|
+
if (value) {
|
59
|
+
this.elem.setAttribute(attrName, value);
|
60
|
+
} else {
|
61
|
+
this.elem.removeAttribute(attrName);
|
62
|
+
}
|
63
|
+
}
|
64
|
+
this.overwrittenAttrs = {};
|
90
65
|
}
|
91
66
|
}
|
92
67
|
|
93
68
|
// Push [this.fullPath] to the SVG
|
94
69
|
private addPathToSVG() {
|
95
|
-
if (!this.lastPathStyle ||
|
70
|
+
if (!this.lastPathStyle || this.lastPathString.length === 0) {
|
96
71
|
return;
|
97
72
|
}
|
98
73
|
|
99
74
|
const pathElem = document.createElementNS(svgNameSpace, 'path');
|
100
|
-
pathElem.setAttribute('d',
|
75
|
+
pathElem.setAttribute('d', this.lastPathString.join(' '));
|
101
76
|
|
102
77
|
const style = this.lastPathStyle;
|
103
78
|
pathElem.setAttribute('fill', style.fill.toHexString());
|
@@ -111,26 +86,44 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
111
86
|
this.objectElems?.push(pathElem);
|
112
87
|
}
|
113
88
|
|
114
|
-
public
|
115
|
-
|
89
|
+
public drawPath(pathSpec: RenderablePathSpec) {
|
90
|
+
const style = pathSpec.style;
|
91
|
+
const path = Path.fromRenderable(pathSpec);
|
116
92
|
|
93
|
+
// Try to extend the previous path, if possible
|
94
|
+
if (!style.fill.eq(this.lastPathStyle?.fill) || this.lastPathString.length === 0) {
|
95
|
+
this.addPathToSVG();
|
96
|
+
this.lastPathStyle = style;
|
97
|
+
this.lastPathString = [];
|
98
|
+
}
|
99
|
+
this.lastPathString.push(path.toString());
|
100
|
+
}
|
101
|
+
|
102
|
+
// Apply [elemTransform] to [elem].
|
103
|
+
private transformFrom(elemTransform: Mat33, elem: SVGElement) {
|
104
|
+
let transform = this.getCanvasToScreenTransform().rightMul(elemTransform);
|
117
105
|
const translation = transform.transformVec2(Vec2.zero);
|
118
106
|
transform = transform.rightMul(Mat33.translation(translation.times(-1)));
|
119
107
|
|
120
|
-
|
121
|
-
textElem.appendChild(document.createTextNode(text));
|
122
|
-
textElem.style.transform = `matrix(
|
108
|
+
elem.style.transform = `matrix(
|
123
109
|
${transform.a1}, ${transform.b1},
|
124
110
|
${transform.a2}, ${transform.b2},
|
125
111
|
${transform.a3}, ${transform.b3}
|
126
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
|
+
|
127
122
|
textElem.style.fontFamily = style.fontFamily;
|
128
123
|
textElem.style.fontVariant = style.fontVariant ?? '';
|
129
124
|
textElem.style.fontWeight = style.fontWeight ?? '';
|
130
125
|
textElem.style.fontSize = style.size + 'px';
|
131
126
|
textElem.style.fill = style.renderingStyle.fill.toHexString();
|
132
|
-
textElem.setAttribute('x', `${toRoundedString(translation.x)}`);
|
133
|
-
textElem.setAttribute('y', `${toRoundedString(translation.y)}`);
|
134
127
|
|
135
128
|
if (style.renderingStyle.stroke) {
|
136
129
|
const strokeStyle = style.renderingStyle.stroke;
|
@@ -142,12 +135,23 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
142
135
|
this.objectElems?.push(textElem);
|
143
136
|
}
|
144
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
|
+
|
145
150
|
public startObject(boundingBox: Rect2) {
|
146
151
|
super.startObject(boundingBox);
|
147
152
|
|
148
153
|
// Only accumulate a path within an object
|
149
|
-
this.
|
150
|
-
this.lastPathStart = null;
|
154
|
+
this.lastPathString = [];
|
151
155
|
this.lastPathStyle = null;
|
152
156
|
this.objectElems = [];
|
153
157
|
}
|
@@ -158,7 +162,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
158
162
|
// Don't extend paths across objects
|
159
163
|
this.addPathToSVG();
|
160
164
|
|
161
|
-
if (loaderData) {
|
165
|
+
if (loaderData && !this.sanitize) {
|
162
166
|
// Restore any attributes unsupported by the app.
|
163
167
|
for (const elem of this.objectElems ?? []) {
|
164
168
|
const attrs = loaderData[svgAttributesDataKey] as SVGLoaderUnknownAttribute[]|undefined;
|
@@ -179,49 +183,16 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
179
183
|
}
|
180
184
|
}
|
181
185
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
});
|
189
|
-
}
|
190
|
-
|
191
|
-
protected moveTo(point: Point2) {
|
192
|
-
point = this.canvasToScreen(point);
|
193
|
-
|
194
|
-
this.currentPath!.push({
|
195
|
-
kind: PathCommandType.MoveTo,
|
196
|
-
point,
|
197
|
-
});
|
198
|
-
}
|
199
|
-
|
186
|
+
// Not implemented -- use drawPath instead.
|
187
|
+
private unimplementedMessage() { throw new Error('Not implemenented!'); }
|
188
|
+
protected beginPath(_startPoint: Point2) { this.unimplementedMessage(); }
|
189
|
+
protected endPath(_style: RenderingStyle) { this.unimplementedMessage(); }
|
190
|
+
protected lineTo(_point: Point2) { this.unimplementedMessage(); }
|
191
|
+
protected moveTo(_point: Point2) { this.unimplementedMessage(); }
|
200
192
|
protected traceCubicBezierCurve(
|
201
|
-
|
202
|
-
) {
|
203
|
-
|
204
|
-
controlPoint2 = this.canvasToScreen(controlPoint2);
|
205
|
-
endPoint = this.canvasToScreen(endPoint);
|
206
|
-
|
207
|
-
this.currentPath!.push({
|
208
|
-
kind: PathCommandType.CubicBezierTo,
|
209
|
-
controlPoint1,
|
210
|
-
controlPoint2,
|
211
|
-
endPoint,
|
212
|
-
});
|
213
|
-
}
|
214
|
-
|
215
|
-
protected traceQuadraticBezierCurve(controlPoint: Point2, endPoint: Point2) {
|
216
|
-
controlPoint = this.canvasToScreen(controlPoint);
|
217
|
-
endPoint = this.canvasToScreen(endPoint);
|
218
|
-
|
219
|
-
this.currentPath!.push({
|
220
|
-
kind: PathCommandType.QuadraticBezierTo,
|
221
|
-
controlPoint,
|
222
|
-
endPoint,
|
223
|
-
});
|
224
|
-
}
|
193
|
+
_controlPoint1: Point2, _controlPoint2: Point2, _endPoint: Point2
|
194
|
+
) { this.unimplementedMessage(); }
|
195
|
+
protected traceQuadraticBezierCurve(_controlPoint: Point2, _endPoint: Point2) { this.unimplementedMessage(); }
|
225
196
|
|
226
197
|
public drawPoints(...points: Point2[]) {
|
227
198
|
points.map(point => {
|
@@ -235,6 +206,10 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
235
206
|
|
236
207
|
// Renders a **copy** of the given element.
|
237
208
|
public drawSVGElem(elem: SVGElement) {
|
209
|
+
if (this.sanitize) {
|
210
|
+
return;
|
211
|
+
}
|
212
|
+
|
238
213
|
this.elem.appendChild(elem.cloneNode(true));
|
239
214
|
}
|
240
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
|
}
|
@@ -17,7 +17,6 @@ import HandToolWidget from './widgets/HandToolWidget';
|
|
17
17
|
import BaseWidget from './widgets/BaseWidget';
|
18
18
|
import { EraserTool, PenTool } from '../tools/lib';
|
19
19
|
|
20
|
-
|
21
20
|
export const toolbarCSSPrefix = 'toolbar-';
|
22
21
|
|
23
22
|
type UpdateColorisCallback = ()=>void;
|
@@ -28,6 +27,7 @@ export default class HTMLToolbar {
|
|
28
27
|
private static colorisStarted: boolean = false;
|
29
28
|
private updateColoris: UpdateColorisCallback|null = null;
|
30
29
|
|
30
|
+
/** @internal */
|
31
31
|
public constructor(
|
32
32
|
private editor: Editor, parent: HTMLElement,
|
33
33
|
private localizationTable: ToolbarLocalization = defaultToolbarLocalization,
|