js-draw 0.2.3 → 0.3.1
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/CHANGELOG.md +11 -0
- package/dist/bundle.js +1 -1
- package/dist/src/EditorImage.js +0 -1
- package/dist/src/components/Stroke.js +11 -6
- package/dist/src/components/builders/FreehandLineBuilder.js +6 -6
- package/dist/src/components/lib.d.ts +2 -0
- package/dist/src/components/lib.js +2 -0
- package/dist/src/lib.d.ts +5 -1
- package/dist/src/lib.js +5 -1
- package/dist/src/math/LineSegment2.d.ts +2 -0
- package/dist/src/math/LineSegment2.js +6 -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 +3 -3
- package/dist/src/rendering/caching/RenderingCacheNode.js +2 -1
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/CanvasRenderer.js +6 -6
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +10 -11
- package/dist/src/rendering/renderers/SVGRenderer.js +27 -68
- package/dist/src/toolbar/HTMLToolbar.d.ts +5 -1
- package/dist/src/toolbar/HTMLToolbar.js +30 -31
- package/dist/src/toolbar/icons.d.ts +1 -1
- package/dist/src/toolbar/icons.js +4 -0
- package/dist/src/toolbar/lib.d.ts +3 -0
- package/dist/src/toolbar/lib.js +4 -0
- package/dist/src/toolbar/makeColorInput.js +2 -2
- package/dist/src/toolbar/types.d.ts +0 -4
- package/dist/src/toolbar/types.js +1 -5
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +3 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +21 -1
- package/dist/src/toolbar/widgets/{EraserWidget.d.ts → EraserToolWidget.d.ts} +1 -1
- package/dist/src/toolbar/widgets/{EraserWidget.js → EraserToolWidget.js} +1 -1
- package/dist/src/toolbar/widgets/{PenWidget.d.ts → PenToolWidget.d.ts} +2 -3
- package/dist/src/toolbar/widgets/{PenWidget.js → PenToolWidget.js} +6 -7
- package/dist/src/toolbar/widgets/{SelectionWidget.d.ts → SelectionToolWidget.d.ts} +1 -1
- package/dist/src/toolbar/widgets/{SelectionWidget.js → SelectionToolWidget.js} +1 -1
- package/dist/src/toolbar/widgets/lib.d.ts +8 -0
- package/dist/src/toolbar/widgets/lib.js +8 -0
- package/dist/src/tools/BaseTool.d.ts +1 -2
- package/dist/src/tools/BaseTool.js +6 -0
- package/dist/src/tools/Eraser.d.ts +0 -2
- package/dist/src/tools/Eraser.js +0 -2
- package/dist/src/tools/PanZoom.d.ts +0 -2
- package/dist/src/tools/PanZoom.js +0 -2
- package/dist/src/tools/Pen.d.ts +9 -9
- package/dist/src/tools/Pen.js +23 -6
- package/dist/src/tools/PipetteTool.d.ts +0 -2
- package/dist/src/tools/PipetteTool.js +0 -2
- package/dist/src/tools/SelectionTool.d.ts +0 -2
- package/dist/src/tools/SelectionTool.js +4 -3
- package/dist/src/tools/TextTool.d.ts +0 -2
- package/dist/src/tools/TextTool.js +0 -2
- package/dist/src/tools/ToolController.d.ts +8 -11
- package/dist/src/tools/ToolController.js +37 -18
- package/dist/src/tools/ToolEnabledGroup.js +1 -1
- package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
- package/dist/src/tools/ToolSwitcherShortcut.js +26 -0
- package/dist/src/tools/UndoRedoShortcut.d.ts +0 -2
- package/dist/src/tools/UndoRedoShortcut.js +3 -2
- package/dist/src/tools/lib.d.ts +13 -0
- package/dist/src/tools/lib.js +13 -0
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/tools/localization.js +1 -0
- package/dist/src/types.d.ts +8 -2
- package/dist/src/types.js +1 -0
- package/package.json +2 -2
- package/src/EditorImage.ts +0 -1
- package/src/components/Stroke.test.ts +5 -0
- package/src/components/Stroke.ts +13 -7
- package/src/components/builders/FreehandLineBuilder.ts +6 -6
- package/src/components/lib.ts +3 -0
- package/src/lib.ts +5 -1
- package/src/math/LineSegment2.ts +8 -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 +3 -3
- package/src/rendering/caching/RenderingCacheNode.ts +3 -1
- package/src/rendering/renderers/AbstractRenderer.ts +1 -0
- package/src/rendering/renderers/CanvasRenderer.ts +6 -6
- package/src/rendering/renderers/SVGRenderer.ts +30 -84
- package/src/toolbar/HTMLToolbar.ts +35 -38
- package/src/toolbar/icons.ts +5 -1
- package/src/toolbar/lib.ts +4 -0
- package/src/toolbar/makeColorInput.ts +1 -2
- package/src/toolbar/types.ts +1 -5
- package/src/toolbar/widgets/BaseWidget.ts +27 -1
- package/src/toolbar/widgets/{EraserWidget.ts → EraserToolWidget.ts} +1 -1
- package/src/toolbar/widgets/{PenWidget.ts → PenToolWidget.ts} +10 -9
- package/src/toolbar/widgets/{SelectionWidget.ts → SelectionToolWidget.ts} +1 -1
- package/src/toolbar/widgets/lib.ts +10 -0
- package/src/tools/BaseTool.ts +8 -3
- package/src/tools/Eraser.ts +0 -2
- package/src/tools/PanZoom.ts +0 -2
- package/src/tools/Pen.ts +32 -13
- package/src/tools/PipetteTool.ts +0 -3
- package/src/tools/SelectionTool.test.ts +1 -2
- package/src/tools/SelectionTool.ts +5 -3
- package/src/tools/TextTool.ts +0 -2
- package/src/tools/ToolController.ts +44 -20
- package/src/tools/ToolEnabledGroup.ts +1 -1
- package/src/tools/ToolSwitcherShortcut.ts +34 -0
- package/src/tools/UndoRedoShortcut.ts +4 -4
- package/src/tools/lib.ts +18 -0
- package/src/tools/localization.ts +2 -0
- package/src/types.ts +13 -1
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:
|
80
|
-
maxScale: 1.
|
81
|
-
minComponentsPerCache:
|
79
|
+
cacheSize: 600 * 600 * 4 * 90,
|
80
|
+
maxScale: 1.4,
|
81
|
+
minComponentsPerCache: 20,
|
82
82
|
minComponentsToUseCache: 105,
|
83
83
|
});
|
84
84
|
|
@@ -221,11 +221,13 @@ export default class RenderingCacheNode {
|
|
221
221
|
}
|
222
222
|
} else {
|
223
223
|
// Determine whether we already have rendered the items
|
224
|
+
const tooSmallToRender = (rect: Rect2) => rect.w / this.region.w < 1 / this.cacheState.props.blockResolution.x;
|
225
|
+
|
224
226
|
const leaves = [];
|
225
227
|
for (const item of items) {
|
226
228
|
leaves.push(
|
227
229
|
...item.getLeavesIntersectingRegion(
|
228
|
-
this.region,
|
230
|
+
this.region, tooSmallToRender,
|
229
231
|
)
|
230
232
|
);
|
231
233
|
}
|
@@ -59,13 +59,13 @@ export default class CanvasRenderer extends AbstractRenderer {
|
|
59
59
|
// Set parameters for lower/higher quality rendering
|
60
60
|
public setDraftMode(draftMode: boolean) {
|
61
61
|
if (draftMode) {
|
62
|
-
this.minSquareCurveApproxDist =
|
63
|
-
this.minRenderSizeBothDimens =
|
64
|
-
this.minRenderSizeAnyDimen =
|
62
|
+
this.minSquareCurveApproxDist = 9;
|
63
|
+
this.minRenderSizeBothDimens = 2;
|
64
|
+
this.minRenderSizeAnyDimen = 0.5;
|
65
65
|
} else {
|
66
|
-
this.minSquareCurveApproxDist =
|
67
|
-
this.minRenderSizeBothDimens = 0.
|
68
|
-
this.minRenderSizeAnyDimen =
|
66
|
+
this.minSquareCurveApproxDist = 0.5;
|
67
|
+
this.minRenderSizeBothDimens = 0.3;
|
68
|
+
this.minRenderSizeAnyDimen = 1e-5;
|
69
69
|
}
|
70
70
|
}
|
71
71
|
|
@@ -2,23 +2,19 @@
|
|
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, { 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> = {};
|
@@ -58,46 +54,17 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
58
54
|
}
|
59
55
|
}
|
60
56
|
this.overwrittenAttrs = {};
|
61
|
-
|
62
|
-
|
63
|
-
protected beginPath(startPoint: Point2) {
|
64
|
-
this.currentPath = [];
|
65
|
-
this.pathStart = this.canvasToScreen(startPoint);
|
66
|
-
this.lastPathStart ??= this.pathStart;
|
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;
|
87
|
-
|
88
|
-
this.pathStart = null;
|
89
|
-
this.currentPath = null;
|
90
|
-
}
|
57
|
+
this.lastPathString = [];
|
91
58
|
}
|
92
59
|
|
93
60
|
// Push [this.fullPath] to the SVG
|
94
61
|
private addPathToSVG() {
|
95
|
-
if (!this.lastPathStyle ||
|
62
|
+
if (!this.lastPathStyle || this.lastPathString.length === 0) {
|
96
63
|
return;
|
97
64
|
}
|
98
65
|
|
99
66
|
const pathElem = document.createElementNS(svgNameSpace, 'path');
|
100
|
-
pathElem.setAttribute('d',
|
67
|
+
pathElem.setAttribute('d', this.lastPathString.join(' '));
|
101
68
|
|
102
69
|
const style = this.lastPathStyle;
|
103
70
|
pathElem.setAttribute('fill', style.fill.toHexString());
|
@@ -111,6 +78,19 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
111
78
|
this.objectElems?.push(pathElem);
|
112
79
|
}
|
113
80
|
|
81
|
+
public drawPath(pathSpec: RenderablePathSpec) {
|
82
|
+
const style = pathSpec.style;
|
83
|
+
const path = Path.fromRenderable(pathSpec);
|
84
|
+
|
85
|
+
// Try to extend the previous path, if possible
|
86
|
+
if (!style.fill.eq(this.lastPathStyle?.fill) || this.lastPathString.length === 0) {
|
87
|
+
this.addPathToSVG();
|
88
|
+
this.lastPathStyle = style;
|
89
|
+
this.lastPathString = [];
|
90
|
+
}
|
91
|
+
this.lastPathString.push(path.toString());
|
92
|
+
}
|
93
|
+
|
114
94
|
public drawText(text: string, transform: Mat33, style: TextStyle): void {
|
115
95
|
transform = this.getCanvasToScreenTransform().rightMul(transform);
|
116
96
|
|
@@ -146,8 +126,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
146
126
|
super.startObject(boundingBox);
|
147
127
|
|
148
128
|
// Only accumulate a path within an object
|
149
|
-
this.
|
150
|
-
this.lastPathStart = null;
|
129
|
+
this.lastPathString = [];
|
151
130
|
this.lastPathStyle = null;
|
152
131
|
this.objectElems = [];
|
153
132
|
}
|
@@ -179,49 +158,16 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
179
158
|
}
|
180
159
|
}
|
181
160
|
|
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
|
-
|
161
|
+
// Not implemented -- use drawPath instead.
|
162
|
+
private unimplementedMessage() { throw new Error('Not implemenented!'); }
|
163
|
+
protected beginPath(_startPoint: Point2) { this.unimplementedMessage(); }
|
164
|
+
protected endPath(_style: RenderingStyle) { this.unimplementedMessage(); }
|
165
|
+
protected lineTo(_point: Point2) { this.unimplementedMessage(); }
|
166
|
+
protected moveTo(_point: Point2) { this.unimplementedMessage(); }
|
200
167
|
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
|
-
}
|
168
|
+
_controlPoint1: Point2, _controlPoint2: Point2, _endPoint: Point2
|
169
|
+
) { this.unimplementedMessage(); }
|
170
|
+
protected traceQuadraticBezierCurve(_controlPoint: Point2, _endPoint: Point2) { this.unimplementedMessage(); }
|
225
171
|
|
226
172
|
public drawPoints(...points: Point2[]) {
|
227
173
|
points.map(point => {
|
@@ -1,32 +1,33 @@
|
|
1
1
|
import Editor from '../Editor';
|
2
|
-
import { ToolType } from '../tools/ToolController';
|
3
2
|
import { EditorEventType } from '../types';
|
4
3
|
|
5
4
|
import { coloris, init as colorisInit } from '@melloware/coloris';
|
6
5
|
import Color4 from '../Color4';
|
7
|
-
import Pen from '../tools/Pen';
|
8
|
-
import Eraser from '../tools/Eraser';
|
9
6
|
import SelectionTool from '../tools/SelectionTool';
|
10
7
|
import { defaultToolbarLocalization, ToolbarLocalization } from './localization';
|
11
8
|
import { ActionButtonIcon } from './types';
|
12
9
|
import { makeRedoIcon, makeUndoIcon } from './icons';
|
13
10
|
import PanZoom from '../tools/PanZoom';
|
14
11
|
import TextTool from '../tools/TextTool';
|
15
|
-
import
|
16
|
-
import EraserWidget from './widgets/
|
17
|
-
import
|
12
|
+
import PenToolWidget from './widgets/PenToolWidget';
|
13
|
+
import EraserWidget from './widgets/EraserToolWidget';
|
14
|
+
import SelectionToolWidget from './widgets/SelectionToolWidget';
|
18
15
|
import TextToolWidget from './widgets/TextToolWidget';
|
19
16
|
import HandToolWidget from './widgets/HandToolWidget';
|
20
|
-
|
17
|
+
import BaseWidget from './widgets/BaseWidget';
|
18
|
+
import { EraserTool, PenTool } from '../tools/lib';
|
21
19
|
|
22
20
|
export const toolbarCSSPrefix = 'toolbar-';
|
23
21
|
|
22
|
+
type UpdateColorisCallback = ()=>void;
|
24
23
|
|
25
24
|
export default class HTMLToolbar {
|
26
25
|
private container: HTMLElement;
|
27
26
|
|
28
27
|
private static colorisStarted: boolean = false;
|
28
|
+
private updateColoris: UpdateColorisCallback|null = null;
|
29
29
|
|
30
|
+
/** @internal */
|
30
31
|
public constructor(
|
31
32
|
private editor: Editor, parent: HTMLElement,
|
32
33
|
private localizationTable: ToolbarLocalization = defaultToolbarLocalization,
|
@@ -45,6 +46,12 @@ export default class HTMLToolbar {
|
|
45
46
|
|
46
47
|
// @internal
|
47
48
|
public setupColorPickers() {
|
49
|
+
// Much of the setup only needs to be done once.
|
50
|
+
if (this.updateColoris) {
|
51
|
+
this.updateColoris();
|
52
|
+
return;
|
53
|
+
}
|
54
|
+
|
48
55
|
const closePickerOverlay = document.createElement('div');
|
49
56
|
closePickerOverlay.className = `${toolbarCSSPrefix}closeColorPickerOverlay`;
|
50
57
|
this.editor.createHTMLOverlay(closePickerOverlay);
|
@@ -73,6 +80,7 @@ export default class HTMLToolbar {
|
|
73
80
|
});
|
74
81
|
};
|
75
82
|
initColoris();
|
83
|
+
this.updateColoris = initColoris;
|
76
84
|
|
77
85
|
const addColorToSwatch = (newColor: string) => {
|
78
86
|
let alreadyPresent = false;
|
@@ -110,6 +118,13 @@ export default class HTMLToolbar {
|
|
110
118
|
});
|
111
119
|
}
|
112
120
|
|
121
|
+
// Adds an `ActionButtonWidget` or `BaseToolWidget`. The widget should not have already have a parent
|
122
|
+
// (i.e. its `addTo` method should not have been called).
|
123
|
+
public addWidget(widget: BaseWidget) {
|
124
|
+
widget.addTo(this.container);
|
125
|
+
this.setupColorPickers();
|
126
|
+
}
|
127
|
+
|
113
128
|
public addActionButton(title: string|ActionButtonIcon, command: ()=> void, parent?: Element) {
|
114
129
|
const button = document.createElement('button');
|
115
130
|
button.classList.add(`${toolbarCSSPrefix}button`);
|
@@ -134,7 +149,7 @@ export default class HTMLToolbar {
|
|
134
149
|
return button;
|
135
150
|
}
|
136
151
|
|
137
|
-
|
152
|
+
public addUndoRedoButtons() {
|
138
153
|
const undoRedoGroup = document.createElement('div');
|
139
154
|
undoRedoGroup.classList.add(`${toolbarCSSPrefix}buttonGroup`);
|
140
155
|
|
@@ -166,47 +181,29 @@ export default class HTMLToolbar {
|
|
166
181
|
|
167
182
|
public addDefaultToolWidgets() {
|
168
183
|
const toolController = this.editor.toolController;
|
169
|
-
for (const tool of toolController.getMatchingTools(
|
170
|
-
|
171
|
-
throw new Error('All `Pen` tools must have kind === ToolType.Pen');
|
172
|
-
}
|
173
|
-
|
174
|
-
const widget = new PenWidget(
|
184
|
+
for (const tool of toolController.getMatchingTools(PenTool)) {
|
185
|
+
const widget = new PenToolWidget(
|
175
186
|
this.editor, tool, this.localizationTable,
|
176
187
|
);
|
177
|
-
|
188
|
+
this.addWidget(widget);
|
178
189
|
}
|
179
190
|
|
180
|
-
for (const tool of toolController.getMatchingTools(
|
181
|
-
|
182
|
-
throw new Error('All Erasers must have kind === ToolType.Eraser!');
|
183
|
-
}
|
184
|
-
|
185
|
-
(new EraserWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
191
|
+
for (const tool of toolController.getMatchingTools(EraserTool)) {
|
192
|
+
this.addWidget(new EraserWidget(this.editor, tool, this.localizationTable));
|
186
193
|
}
|
187
194
|
|
188
|
-
for (const tool of toolController.getMatchingTools(
|
189
|
-
|
190
|
-
throw new Error('All SelectionTools must have kind === ToolType.Selection');
|
191
|
-
}
|
192
|
-
|
193
|
-
(new SelectionWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
195
|
+
for (const tool of toolController.getMatchingTools(SelectionTool)) {
|
196
|
+
this.addWidget(new SelectionToolWidget(this.editor, tool, this.localizationTable));
|
194
197
|
}
|
195
198
|
|
196
|
-
for (const tool of toolController.getMatchingTools(
|
197
|
-
|
198
|
-
throw new Error('All text tools must have kind === ToolType.Text');
|
199
|
-
}
|
200
|
-
|
201
|
-
(new TextToolWidget(this.editor, tool, this.localizationTable)).addTo(this.container);
|
199
|
+
for (const tool of toolController.getMatchingTools(TextTool)) {
|
200
|
+
this.addWidget(new TextToolWidget(this.editor, tool, this.localizationTable));
|
202
201
|
}
|
203
202
|
|
204
|
-
const panZoomTool = toolController.getMatchingTools(
|
205
|
-
if (panZoomTool
|
206
|
-
(new HandToolWidget(this.editor, panZoomTool, this.localizationTable))
|
203
|
+
const panZoomTool = toolController.getMatchingTools(PanZoom)[0];
|
204
|
+
if (panZoomTool) {
|
205
|
+
this.addWidget(new HandToolWidget(this.editor, panZoomTool, this.localizationTable));
|
207
206
|
}
|
208
|
-
|
209
|
-
this.setupColorPickers();
|
210
207
|
}
|
211
208
|
|
212
209
|
public addDefaultActionButtons() {
|
package/src/toolbar/icons.ts
CHANGED
@@ -287,7 +287,11 @@ export const makeTextIcon = (textStyle: TextStyle) => {
|
|
287
287
|
return icon;
|
288
288
|
};
|
289
289
|
|
290
|
-
export const makePenIcon = (tipThickness: number, color: string) => {
|
290
|
+
export const makePenIcon = (tipThickness: number, color: string|Color4) => {
|
291
|
+
if (color instanceof Color4) {
|
292
|
+
color = color.toHexString();
|
293
|
+
}
|
294
|
+
|
291
295
|
const icon = document.createElementNS(svgNamespace, 'svg');
|
292
296
|
icon.setAttribute('viewBox', '0 0 100 100');
|
293
297
|
|
@@ -1,7 +1,6 @@
|
|
1
1
|
import Color4 from '../Color4';
|
2
2
|
import Editor from '../Editor';
|
3
3
|
import PipetteTool from '../tools/PipetteTool';
|
4
|
-
import { ToolType } from '../tools/ToolController';
|
5
4
|
import { EditorEventType } from '../types';
|
6
5
|
import { makePipetteIcon } from './icons';
|
7
6
|
|
@@ -77,7 +76,7 @@ const addPipetteTool = (editor: Editor, container: HTMLElement, onColorChange: O
|
|
77
76
|
};
|
78
77
|
updatePipetteIcon();
|
79
78
|
|
80
|
-
const pipetteTool: PipetteTool|undefined = editor.toolController.getMatchingTools(
|
79
|
+
const pipetteTool: PipetteTool|undefined = editor.toolController.getMatchingTools(PipetteTool)[0];
|
81
80
|
const endColorSelectMode = () => {
|
82
81
|
pipetteTool?.clearColorListener();
|
83
82
|
updatePipetteIcon();
|