js-draw 0.3.0 → 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.
Files changed (56) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/components/Stroke.js +11 -6
  4. package/dist/src/components/builders/FreehandLineBuilder.js +5 -5
  5. package/dist/src/math/LineSegment2.d.ts +2 -0
  6. package/dist/src/math/LineSegment2.js +6 -0
  7. package/dist/src/math/Path.d.ts +5 -1
  8. package/dist/src/math/Path.js +89 -7
  9. package/dist/src/math/Rect2.js +1 -1
  10. package/dist/src/math/Triangle.d.ts +11 -0
  11. package/dist/src/math/Triangle.js +19 -0
  12. package/dist/src/rendering/Display.js +2 -2
  13. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +2 -1
  14. package/dist/src/rendering/renderers/SVGRenderer.d.ts +10 -11
  15. package/dist/src/rendering/renderers/SVGRenderer.js +27 -68
  16. package/dist/src/toolbar/HTMLToolbar.d.ts +1 -0
  17. package/dist/src/toolbar/HTMLToolbar.js +1 -0
  18. package/dist/src/toolbar/widgets/BaseWidget.d.ts +3 -0
  19. package/dist/src/toolbar/widgets/BaseWidget.js +21 -1
  20. package/dist/src/tools/BaseTool.d.ts +1 -0
  21. package/dist/src/tools/BaseTool.js +6 -0
  22. package/dist/src/tools/Pen.d.ts +2 -1
  23. package/dist/src/tools/Pen.js +16 -0
  24. package/dist/src/tools/ToolController.d.ts +1 -0
  25. package/dist/src/tools/ToolController.js +9 -2
  26. package/dist/src/tools/ToolSwitcherShortcut.d.ts +8 -0
  27. package/dist/src/tools/ToolSwitcherShortcut.js +26 -0
  28. package/dist/src/tools/lib.d.ts +1 -0
  29. package/dist/src/tools/lib.js +1 -0
  30. package/dist/src/tools/localization.d.ts +1 -0
  31. package/dist/src/tools/localization.js +1 -0
  32. package/dist/src/types.d.ts +8 -2
  33. package/dist/src/types.js +1 -0
  34. package/package.json +2 -2
  35. package/src/components/Stroke.test.ts +5 -0
  36. package/src/components/Stroke.ts +13 -7
  37. package/src/components/builders/FreehandLineBuilder.ts +5 -5
  38. package/src/math/LineSegment2.ts +8 -0
  39. package/src/math/Path.test.ts +53 -0
  40. package/src/math/Path.toString.test.ts +4 -2
  41. package/src/math/Path.ts +109 -11
  42. package/src/math/Rect2.ts +1 -1
  43. package/src/math/Triangle.ts +29 -0
  44. package/src/rendering/Display.ts +2 -2
  45. package/src/rendering/renderers/AbstractRenderer.ts +1 -0
  46. package/src/rendering/renderers/SVGRenderer.ts +30 -84
  47. package/src/toolbar/HTMLToolbar.ts +1 -1
  48. package/src/toolbar/types.ts +1 -1
  49. package/src/toolbar/widgets/BaseWidget.ts +27 -1
  50. package/src/tools/BaseTool.ts +8 -0
  51. package/src/tools/Pen.ts +20 -1
  52. package/src/tools/ToolController.ts +10 -3
  53. package/src/tools/ToolSwitcherShortcut.ts +34 -0
  54. package/src/tools/lib.ts +1 -0
  55. package/src/tools/localization.ts +2 -0
  56. package/src/types.ts +13 -1
@@ -6,7 +6,7 @@ export default class Stroke extends AbstractComponent {
6
6
  constructor(parts) {
7
7
  var _a;
8
8
  super('stroke');
9
- this.parts = parts.map(section => {
9
+ this.parts = parts.map((section) => {
10
10
  const path = Path.fromRenderable(section);
11
11
  const pathBBox = this.bboxForPart(path.bbox, section.style);
12
12
  if (!this.contentBBox) {
@@ -17,7 +17,6 @@ export default class Stroke extends AbstractComponent {
17
17
  }
18
18
  return {
19
19
  path,
20
- bbox: pathBBox,
21
20
  // To implement RenderablePathSpec
22
21
  startPoint: path.startPoint,
23
22
  style: section.style,
@@ -37,10 +36,17 @@ export default class Stroke extends AbstractComponent {
37
36
  render(canvas, visibleRect) {
38
37
  canvas.startObject(this.getBBox());
39
38
  for (const part of this.parts) {
40
- const bbox = part.bbox;
41
- if (!visibleRect || bbox.intersects(visibleRect)) {
42
- canvas.drawPath(part);
39
+ const bbox = this.bboxForPart(part.path.bbox, part.style);
40
+ if (visibleRect) {
41
+ if (!bbox.intersects(visibleRect)) {
42
+ continue;
43
+ }
44
+ const muchBiggerThanVisible = bbox.size.x > visibleRect.size.x * 2 || bbox.size.y > visibleRect.size.y * 2;
45
+ if (muchBiggerThanVisible && !part.path.closedRoughlyIntersects(visibleRect)) {
46
+ continue;
47
+ }
43
48
  }
49
+ canvas.drawPath(part);
44
50
  }
45
51
  canvas.endObject(this.getLoadSaveData());
46
52
  }
@@ -67,7 +73,6 @@ export default class Stroke extends AbstractComponent {
67
73
  }
68
74
  return {
69
75
  path: newPath,
70
- bbox: newBBox,
71
76
  startPoint: newPath.startPoint,
72
77
  commands: newPath.parts,
73
78
  style: part.style,
@@ -7,9 +7,9 @@ import Stroke from '../Stroke';
7
7
  import Viewport from '../../Viewport';
8
8
  export const makeFreehandLineBuilder = (initialPoint, viewport) => {
9
9
  // Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
10
- // less than ± 2 px from the curve.
10
+ // less than ±1 px from the curve.
11
11
  const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 7;
12
- const minSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 2;
12
+ const minSmoothingDist = viewport.getSizeOfPixelOnCanvas();
13
13
  return new FreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist);
14
14
  };
15
15
  // Handles stroke smoothing and creates Strokes from user/stylus input.
@@ -136,7 +136,7 @@ export default class FreehandLineBuilder {
136
136
  if (!this.isFirstSegment) {
137
137
  return;
138
138
  }
139
- const width = Viewport.roundPoint(this.startPoint.width / 3.5, this.minFitAllowed);
139
+ const width = Viewport.roundPoint(this.startPoint.width / 3.5, Math.min(this.minFitAllowed, this.startPoint.width / 4));
140
140
  const center = this.roundPoint(this.startPoint.pos);
141
141
  // Start on the right, cycle clockwise:
142
142
  // |
@@ -362,7 +362,7 @@ export default class FreehandLineBuilder {
362
362
  for (const point of this.buffer) {
363
363
  const proj = Vec2.ofXY(curve.project(point.xy));
364
364
  const dist = proj.minus(point).magnitude();
365
- const minFit = Math.max(Math.min(this.curveStartWidth, this.curveEndWidth) / 2, this.minFitAllowed);
365
+ const minFit = Math.max(Math.min(this.curveStartWidth, this.curveEndWidth) / 3, this.minFitAllowed);
366
366
  if (dist > minFit || dist > this.maxFitAllowed) {
367
367
  return false;
368
368
  }
@@ -370,7 +370,7 @@ export default class FreehandLineBuilder {
370
370
  return true;
371
371
  };
372
372
  const approxCurveLen = controlPoint.minus(segmentStart).magnitude() + segmentEnd.minus(controlPoint).magnitude();
373
- if (this.buffer.length > 3 && approxCurveLen > this.curveEndWidth / 2) {
373
+ if (this.buffer.length > 3 && approxCurveLen > this.curveEndWidth / 3) {
374
374
  if (!curveMatchesPoints(this.currentCurve)) {
375
375
  // Use a curve that better fits the points
376
376
  this.currentCurve = prevCurve;
@@ -15,6 +15,8 @@ export default class LineSegment2 {
15
15
  get p2(): Point2;
16
16
  get(t: number): Point2;
17
17
  intersection(other: LineSegment2): IntersectionResult | null;
18
+ intersects(other: LineSegment2): boolean;
18
19
  closestPointTo(target: Point2): import("./Vec3").default;
20
+ toString(): string;
19
21
  }
20
22
  export {};
@@ -97,6 +97,9 @@ export default class LineSegment2 {
97
97
  t: resultT,
98
98
  };
99
99
  }
100
+ intersects(other) {
101
+ return this.intersection(other) !== null;
102
+ }
100
103
  // Returns the closest point on this to [target]
101
104
  closestPointTo(target) {
102
105
  // Distance from P1 along this' direction.
@@ -113,4 +116,7 @@ export default class LineSegment2 {
113
116
  return this.p1;
114
117
  }
115
118
  }
119
+ toString() {
120
+ return `LineSegment(${this.p1.toString()}, ${this.p2.toString()})`;
121
+ }
116
122
  }
@@ -39,18 +39,22 @@ interface IntersectionResult {
39
39
  export default class Path {
40
40
  readonly startPoint: Point2;
41
41
  readonly parts: PathCommand[];
42
- private cachedGeometry;
43
42
  readonly bbox: Rect2;
44
43
  constructor(startPoint: Point2, parts: PathCommand[]);
44
+ private cachedGeometry;
45
45
  get geometry(): Array<LineSegment2 | Bezier>;
46
+ private cachedPolylineApproximation;
47
+ polylineApproximation(): LineSegment2[];
46
48
  static computeBBoxForSegment(startPoint: Point2, part: PathCommand): Rect2;
47
49
  intersection(line: LineSegment2): IntersectionResult[];
48
50
  mapPoints(mapping: (point: Point2) => Point2): Path;
49
51
  transformedBy(affineTransfm: Mat33): Path;
50
52
  union(other: Path | null): Path;
53
+ closedRoughlyIntersects(rect: Rect2): boolean;
51
54
  static fromRect(rect: Rect2, lineWidth?: number | null): Path;
52
55
  static fromRenderable(renderable: RenderablePathSpec): Path;
53
56
  toRenderable(fill: RenderingStyle): RenderablePathSpec;
57
+ private cachedStringVersion;
54
58
  toString(): string;
55
59
  serialize(): string;
56
60
  static toString(startPoint: Point2, parts: PathCommand[], onlyAbsCommands?: boolean): string;
@@ -15,6 +15,8 @@ export default class Path {
15
15
  this.startPoint = startPoint;
16
16
  this.parts = parts;
17
17
  this.cachedGeometry = null;
18
+ this.cachedPolylineApproximation = null;
19
+ this.cachedStringVersion = null;
18
20
  // Initial bounding box contains one point: the start point.
19
21
  this.bbox = Rect2.bboxOf([startPoint]);
20
22
  // Convert into a representation of the geometry (cache for faster intersection
@@ -52,6 +54,34 @@ export default class Path {
52
54
  this.cachedGeometry = geometry;
53
55
  return this.cachedGeometry;
54
56
  }
57
+ // Approximates this path with a group of line segments.
58
+ polylineApproximation() {
59
+ if (this.cachedPolylineApproximation) {
60
+ return this.cachedPolylineApproximation;
61
+ }
62
+ const points = [];
63
+ for (const part of this.parts) {
64
+ switch (part.kind) {
65
+ case PathCommandType.CubicBezierTo:
66
+ points.push(part.controlPoint1, part.controlPoint2, part.endPoint);
67
+ break;
68
+ case PathCommandType.QuadraticBezierTo:
69
+ points.push(part.controlPoint, part.endPoint);
70
+ break;
71
+ case PathCommandType.MoveTo:
72
+ case PathCommandType.LineTo:
73
+ points.push(part.point);
74
+ break;
75
+ }
76
+ }
77
+ const result = [];
78
+ let prevPoint = this.startPoint;
79
+ for (const point of points) {
80
+ result.push(new LineSegment2(prevPoint, point));
81
+ prevPoint = point;
82
+ }
83
+ return result;
84
+ }
55
85
  static computeBBoxForSegment(startPoint, part) {
56
86
  const points = [startPoint];
57
87
  let exhaustivenessCheck;
@@ -73,6 +103,9 @@ export default class Path {
73
103
  return Rect2.bboxOf(points);
74
104
  }
75
105
  intersection(line) {
106
+ if (!line.bbox.intersects(this.bbox)) {
107
+ return [];
108
+ }
76
109
  const result = [];
77
110
  for (const part of this.geometry) {
78
111
  if (part instanceof LineSegment2) {
@@ -162,6 +195,47 @@ export default class Path {
162
195
  ...other.parts,
163
196
  ]);
164
197
  }
198
+ // Treats this as a closed path and returns true if part of `rect` is roughly within
199
+ // this path's interior.
200
+ //
201
+ // Note: Assumes that this is a closed, non-self-intersecting path.
202
+ closedRoughlyIntersects(rect) {
203
+ if (rect.containsRect(this.bbox)) {
204
+ return true;
205
+ }
206
+ // Choose a point outside of the path.
207
+ const startPt = this.bbox.topLeft.minus(Vec2.of(1, 1));
208
+ const testPts = rect.corners;
209
+ const polygon = this.polylineApproximation();
210
+ for (const point of testPts) {
211
+ const testLine = new LineSegment2(point, startPt);
212
+ let intersectionCount = 0;
213
+ for (const line of polygon) {
214
+ if (line.intersects(testLine)) {
215
+ intersectionCount++;
216
+ }
217
+ }
218
+ // Odd? The point is within the polygon!
219
+ if (intersectionCount % 2 === 1) {
220
+ return true;
221
+ }
222
+ }
223
+ // Grow the rectangle for possible additional precision.
224
+ const grownRect = rect.grownBy(Math.min(rect.size.x, rect.size.y));
225
+ const edges = [];
226
+ for (const subrect of grownRect.divideIntoGrid(4, 4)) {
227
+ edges.push(...subrect.getEdges());
228
+ }
229
+ for (const edge of edges) {
230
+ for (const line of polygon) {
231
+ if (edge.intersects(line)) {
232
+ return true;
233
+ }
234
+ }
235
+ }
236
+ // Even? Probably no intersection.
237
+ return false;
238
+ }
165
239
  // Returns a path that outlines [rect]. If [lineWidth] is not given, the resultant path is
166
240
  // the outline of [rect]. Otherwise, the resultant path represents a line of width [lineWidth]
167
241
  // that traces [rect].
@@ -195,6 +269,9 @@ export default class Path {
195
269
  return new Path(startPoint, commands);
196
270
  }
197
271
  static fromRenderable(renderable) {
272
+ if (renderable.path) {
273
+ return renderable.path;
274
+ }
198
275
  return new Path(renderable.startPoint, renderable.commands);
199
276
  }
200
277
  toRenderable(fill) {
@@ -202,22 +279,25 @@ export default class Path {
202
279
  startPoint: this.startPoint,
203
280
  style: fill,
204
281
  commands: this.parts,
282
+ path: this,
205
283
  };
206
284
  }
207
285
  toString() {
286
+ if (this.cachedStringVersion) {
287
+ return this.cachedStringVersion;
288
+ }
208
289
  // Hueristic: Try to determine whether converting absolute to relative commands is worth it.
209
- // If we're near (0, 0), it probably isn't worth it and if bounding boxes are large,
210
- // it also probably isn't worth it.
211
- const makeRelativeCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.size.x) < 2
212
- && Math.abs(this.bbox.topLeft.y) > 10 && Math.abs(this.bbox.size.y) < 2;
213
- return Path.toString(this.startPoint, this.parts, !makeRelativeCommands);
290
+ const makeRelativeCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.topLeft.y) > 10;
291
+ const result = Path.toString(this.startPoint, this.parts, !makeRelativeCommands);
292
+ this.cachedStringVersion = result;
293
+ return result;
214
294
  }
215
295
  serialize() {
216
296
  return this.toString();
217
297
  }
218
298
  // @param onlyAbsCommands - True if we should avoid converting absolute coordinates to relative offsets -- such
219
299
  // conversions can lead to smaller output strings, but also take time.
220
- static toString(startPoint, parts, onlyAbsCommands = true) {
300
+ static toString(startPoint, parts, onlyAbsCommands) {
221
301
  const result = [];
222
302
  let prevPoint;
223
303
  const addCommand = (command, ...points) => {
@@ -452,7 +532,9 @@ export default class Path {
452
532
  lastPos = allArgs[allArgs.length - 1];
453
533
  }
454
534
  }
455
- return new Path(startPos !== null && startPos !== void 0 ? startPos : Vec2.zero, commands);
535
+ const result = new Path(startPos !== null && startPos !== void 0 ? startPos : Vec2.zero, commands);
536
+ result.cachedStringVersion = pathString;
537
+ return result;
456
538
  }
457
539
  }
458
540
  Path.empty = new Path(Vec2.zero, []);
@@ -1,6 +1,6 @@
1
1
  import LineSegment2 from './LineSegment2';
2
2
  import { Vec2 } from './Vec2';
3
- // invariant: w > 0, h > 0.
3
+ // invariant: w 0, h 0.
4
4
  export default class Rect2 {
5
5
  constructor(x, y, w, h) {
6
6
  this.x = x;
@@ -0,0 +1,11 @@
1
+ import Mat33 from './Mat33';
2
+ import Vec3 from './Vec3';
3
+ export default class Triangle {
4
+ readonly vertex1: Vec3;
5
+ readonly vertex2: Vec3;
6
+ readonly vertex3: Vec3;
7
+ constructor(vertex1: Vec3, vertex2: Vec3, vertex3: Vec3);
8
+ map(mapping: (vertex: Vec3) => Vec3): Triangle;
9
+ transformed2DBy(affineTransform: Mat33): Triangle;
10
+ transformedBy(linearTransform: Mat33): Triangle;
11
+ }
@@ -0,0 +1,19 @@
1
+ export default class Triangle {
2
+ constructor(vertex1, vertex2, vertex3) {
3
+ this.vertex1 = vertex1;
4
+ this.vertex2 = vertex2;
5
+ this.vertex3 = vertex3;
6
+ }
7
+ map(mapping) {
8
+ return new Triangle(mapping(this.vertex1), mapping(this.vertex2), mapping(this.vertex3));
9
+ }
10
+ // Transform, treating this as composed of 2D points.
11
+ transformed2DBy(affineTransform) {
12
+ return this.map(affineTransform.transformVec2);
13
+ }
14
+ // Transforms this by a linear transform --- verticies are treated as
15
+ // 3D points.
16
+ transformedBy(linearTransform) {
17
+ return this.map(linearTransform.transformVec3);
18
+ }
19
+ }
@@ -71,9 +71,9 @@ export default class Display {
71
71
  return this.dryInkRenderer.canRenderFromWithoutDataLoss(renderer);
72
72
  },
73
73
  blockResolution: cacheBlockResolution,
74
- cacheSize: 600 * 600 * 4 * 120,
74
+ cacheSize: 600 * 600 * 4 * 90,
75
75
  maxScale: 1.4,
76
- minComponentsPerCache: 45,
76
+ minComponentsPerCache: 20,
77
77
  minComponentsToUseCache: 105,
78
78
  });
79
79
  this.editor.notifier.on(EditorEventType.DisplayResized, event => {
@@ -1,7 +1,7 @@
1
1
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
2
2
  import { TextStyle } from '../../components/Text';
3
3
  import Mat33 from '../../math/Mat33';
4
- import { PathCommand } from '../../math/Path';
4
+ import Path, { PathCommand } from '../../math/Path';
5
5
  import Rect2 from '../../math/Rect2';
6
6
  import { Point2, Vec2 } from '../../math/Vec2';
7
7
  import Viewport from '../../Viewport';
@@ -10,6 +10,7 @@ export interface RenderablePathSpec {
10
10
  startPoint: Point2;
11
11
  commands: PathCommand[];
12
12
  style: RenderingStyle;
13
+ path?: Path;
13
14
  }
14
15
  export default abstract class AbstractRenderer {
15
16
  private viewport;
@@ -5,30 +5,29 @@ 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 from './AbstractRenderer';
8
+ import AbstractRenderer, { RenderablePathSpec } from './AbstractRenderer';
9
9
  export default class SVGRenderer extends AbstractRenderer {
10
10
  private elem;
11
- private currentPath;
12
- private pathStart;
13
11
  private lastPathStyle;
14
- private lastPath;
15
- private lastPathStart;
12
+ private lastPathString;
16
13
  private objectElems;
17
14
  private overwrittenAttrs;
18
15
  constructor(elem: SVGSVGElement, viewport: Viewport);
19
16
  setRootSVGAttribute(name: string, value: string | null): void;
20
17
  displaySize(): Vec2;
21
18
  clear(): void;
22
- protected beginPath(startPoint: Point2): void;
23
- protected endPath(style: RenderingStyle): void;
24
19
  private addPathToSVG;
20
+ drawPath(pathSpec: RenderablePathSpec): void;
25
21
  drawText(text: string, transform: Mat33, style: TextStyle): void;
26
22
  startObject(boundingBox: Rect2): void;
27
23
  endObject(loaderData?: LoadSaveDataTable): void;
28
- protected lineTo(point: Point2): void;
29
- protected moveTo(point: Point2): void;
30
- protected traceCubicBezierCurve(controlPoint1: Point2, controlPoint2: Point2, endPoint: Point2): void;
31
- protected traceQuadraticBezierCurve(controlPoint: Point2, endPoint: Point2): void;
24
+ private unimplementedMessage;
25
+ protected beginPath(_startPoint: Point2): void;
26
+ protected endPath(_style: RenderingStyle): void;
27
+ protected lineTo(_point: Point2): void;
28
+ protected moveTo(_point: Point2): void;
29
+ protected traceCubicBezierCurve(_controlPoint1: Point2, _controlPoint2: Point2, _endPoint: Point2): void;
30
+ protected traceQuadraticBezierCurve(_controlPoint: Point2, _endPoint: Point2): void;
32
31
  drawPoints(...points: Point2[]): void;
33
32
  drawSVGElem(elem: SVGElement): void;
34
33
  isTooSmallToRender(_rect: Rect2): boolean;
@@ -1,5 +1,5 @@
1
1
  import Mat33 from '../../math/Mat33';
2
- import Path, { PathCommandType } from '../../math/Path';
2
+ import Path from '../../math/Path';
3
3
  import { toRoundedString } from '../../math/rounding';
4
4
  import { Vec2 } from '../../math/Vec2';
5
5
  import { svgAttributesDataKey, svgStyleAttributesDataKey } from '../../SVGLoader';
@@ -9,6 +9,8 @@ export default class SVGRenderer extends AbstractRenderer {
9
9
  constructor(elem, viewport) {
10
10
  super(viewport);
11
11
  this.elem = elem;
12
+ this.lastPathStyle = null;
13
+ this.lastPathString = [];
12
14
  this.objectElems = null;
13
15
  this.overwrittenAttrs = {};
14
16
  this.clear();
@@ -41,44 +43,16 @@ export default class SVGRenderer extends AbstractRenderer {
41
43
  }
42
44
  }
43
45
  this.overwrittenAttrs = {};
44
- }
45
- beginPath(startPoint) {
46
- var _a;
47
- this.currentPath = [];
48
- this.pathStart = this.canvasToScreen(startPoint);
49
- (_a = this.lastPathStart) !== null && _a !== void 0 ? _a : (this.lastPathStart = this.pathStart);
50
- }
51
- endPath(style) {
52
- var _a;
53
- if (this.currentPath == null) {
54
- throw new Error('No path exists to end! Make sure beginPath was called!');
55
- }
56
- // Try to extend the previous path, if possible
57
- if (style.fill.eq((_a = this.lastPathStyle) === null || _a === void 0 ? void 0 : _a.fill) && this.lastPath != null) {
58
- this.lastPath.push({
59
- kind: PathCommandType.MoveTo,
60
- point: this.pathStart,
61
- }, ...this.currentPath);
62
- this.pathStart = null;
63
- this.currentPath = null;
64
- }
65
- else {
66
- this.addPathToSVG();
67
- this.lastPathStart = this.pathStart;
68
- this.lastPathStyle = style;
69
- this.lastPath = this.currentPath;
70
- this.pathStart = null;
71
- this.currentPath = null;
72
- }
46
+ this.lastPathString = [];
73
47
  }
74
48
  // Push [this.fullPath] to the SVG
75
49
  addPathToSVG() {
76
50
  var _a;
77
- if (!this.lastPathStyle || !this.lastPath) {
51
+ if (!this.lastPathStyle || this.lastPathString.length === 0) {
78
52
  return;
79
53
  }
80
54
  const pathElem = document.createElementNS(svgNameSpace, 'path');
81
- pathElem.setAttribute('d', Path.toString(this.lastPathStart, this.lastPath));
55
+ pathElem.setAttribute('d', this.lastPathString.join(' '));
82
56
  const style = this.lastPathStyle;
83
57
  pathElem.setAttribute('fill', style.fill.toHexString());
84
58
  if (style.stroke) {
@@ -88,6 +62,18 @@ export default class SVGRenderer extends AbstractRenderer {
88
62
  this.elem.appendChild(pathElem);
89
63
  (_a = this.objectElems) === null || _a === void 0 ? void 0 : _a.push(pathElem);
90
64
  }
65
+ drawPath(pathSpec) {
66
+ var _a;
67
+ const style = pathSpec.style;
68
+ const path = Path.fromRenderable(pathSpec);
69
+ // Try to extend the previous path, if possible
70
+ if (!style.fill.eq((_a = this.lastPathStyle) === null || _a === void 0 ? void 0 : _a.fill) || this.lastPathString.length === 0) {
71
+ this.addPathToSVG();
72
+ this.lastPathStyle = style;
73
+ this.lastPathString = [];
74
+ }
75
+ this.lastPathString.push(path.toString());
76
+ }
91
77
  drawText(text, transform, style) {
92
78
  var _a, _b, _c;
93
79
  transform = this.getCanvasToScreenTransform().rightMul(transform);
@@ -118,8 +104,7 @@ export default class SVGRenderer extends AbstractRenderer {
118
104
  startObject(boundingBox) {
119
105
  super.startObject(boundingBox);
120
106
  // Only accumulate a path within an object
121
- this.lastPath = null;
122
- this.lastPathStart = null;
107
+ this.lastPathString = [];
123
108
  this.lastPathStyle = null;
124
109
  this.objectElems = [];
125
110
  }
@@ -146,40 +131,14 @@ export default class SVGRenderer extends AbstractRenderer {
146
131
  }
147
132
  }
148
133
  }
149
- lineTo(point) {
150
- point = this.canvasToScreen(point);
151
- this.currentPath.push({
152
- kind: PathCommandType.LineTo,
153
- point,
154
- });
155
- }
156
- moveTo(point) {
157
- point = this.canvasToScreen(point);
158
- this.currentPath.push({
159
- kind: PathCommandType.MoveTo,
160
- point,
161
- });
162
- }
163
- traceCubicBezierCurve(controlPoint1, controlPoint2, endPoint) {
164
- controlPoint1 = this.canvasToScreen(controlPoint1);
165
- controlPoint2 = this.canvasToScreen(controlPoint2);
166
- endPoint = this.canvasToScreen(endPoint);
167
- this.currentPath.push({
168
- kind: PathCommandType.CubicBezierTo,
169
- controlPoint1,
170
- controlPoint2,
171
- endPoint,
172
- });
173
- }
174
- traceQuadraticBezierCurve(controlPoint, endPoint) {
175
- controlPoint = this.canvasToScreen(controlPoint);
176
- endPoint = this.canvasToScreen(endPoint);
177
- this.currentPath.push({
178
- kind: PathCommandType.QuadraticBezierTo,
179
- controlPoint,
180
- endPoint,
181
- });
182
- }
134
+ // Not implemented -- use drawPath instead.
135
+ unimplementedMessage() { throw new Error('Not implemenented!'); }
136
+ beginPath(_startPoint) { this.unimplementedMessage(); }
137
+ endPath(_style) { this.unimplementedMessage(); }
138
+ lineTo(_point) { this.unimplementedMessage(); }
139
+ moveTo(_point) { this.unimplementedMessage(); }
140
+ traceCubicBezierCurve(_controlPoint1, _controlPoint2, _endPoint) { this.unimplementedMessage(); }
141
+ traceQuadraticBezierCurve(_controlPoint, _endPoint) { this.unimplementedMessage(); }
183
142
  drawPoints(...points) {
184
143
  points.map(point => {
185
144
  const elem = document.createElementNS(svgNameSpace, 'circle');
@@ -9,6 +9,7 @@ export default class HTMLToolbar {
9
9
  private container;
10
10
  private static colorisStarted;
11
11
  private updateColoris;
12
+ /** @internal */
12
13
  constructor(editor: Editor, parent: HTMLElement, localizationTable?: ToolbarLocalization);
13
14
  setupColorPickers(): void;
14
15
  addWidget(widget: BaseWidget): void;
@@ -14,6 +14,7 @@ import HandToolWidget from './widgets/HandToolWidget';
14
14
  import { EraserTool, PenTool } from '../tools/lib';
15
15
  export const toolbarCSSPrefix = 'toolbar-';
16
16
  export default class HTMLToolbar {
17
+ /** @internal */
17
18
  constructor(editor, parent, localizationTable = defaultToolbarLocalization) {
18
19
  this.editor = editor;
19
20
  this.localizationTable = localizationTable;
@@ -12,6 +12,7 @@ export default abstract class BaseWidget {
12
12
  private label;
13
13
  private disabled;
14
14
  private subWidgets;
15
+ private toplevel;
15
16
  constructor(editor: Editor, localizationTable: ToolbarLocalization);
16
17
  protected abstract getTitle(): string;
17
18
  protected abstract createIcon(): Element;
@@ -26,6 +27,8 @@ export default abstract class BaseWidget {
26
27
  setSelected(selected: boolean): void;
27
28
  protected setDropdownVisible(visible: boolean): void;
28
29
  protected repositionDropdown(): void;
30
+ /** Set whether the widget is contained within another. @internal */
31
+ protected setIsToplevel(toplevel: boolean): void;
29
32
  protected isDropdownVisible(): boolean;
30
33
  protected isSelected(): boolean;
31
34
  private createDropdownIcon;
@@ -10,7 +10,7 @@ var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (
10
10
  return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
11
11
  };
12
12
  var _BaseWidget_hasDropdown;
13
- import { InputEvtType } from '../../types';
13
+ import { EditorEventType, InputEvtType } from '../../types';
14
14
  import { toolbarCSSPrefix } from '../HTMLToolbar';
15
15
  import { makeDropdownIcon } from '../icons';
16
16
  export default class BaseWidget {
@@ -20,6 +20,7 @@ export default class BaseWidget {
20
20
  _BaseWidget_hasDropdown.set(this, void 0);
21
21
  this.disabled = false;
22
22
  this.subWidgets = [];
23
+ this.toplevel = true;
23
24
  this.icon = null;
24
25
  this.container = document.createElement('div');
25
26
  this.container.classList.add(`${toolbarCSSPrefix}toolContainer`);
@@ -41,6 +42,7 @@ export default class BaseWidget {
41
42
  }
42
43
  for (const widget of this.subWidgets) {
43
44
  widget.addTo(dropdown);
45
+ widget.setIsToplevel(false);
44
46
  }
45
47
  return true;
46
48
  }
@@ -87,6 +89,7 @@ export default class BaseWidget {
87
89
  this.subWidgets.push(widget);
88
90
  }
89
91
  // Adds this to [parent]. This can only be called once for each ToolbarWidget.
92
+ // @internal
90
93
  addTo(parent) {
91
94
  this.label.innerText = this.getTitle();
92
95
  this.setupActionBtnClickListener(this.button);
@@ -99,6 +102,15 @@ export default class BaseWidget {
99
102
  this.dropdownIcon = this.createDropdownIcon();
100
103
  this.button.appendChild(this.dropdownIcon);
101
104
  this.container.appendChild(this.dropdownContainer);
105
+ this.editor.notifier.on(EditorEventType.ToolbarDropdownShown, (evt) => {
106
+ if (evt.kind === EditorEventType.ToolbarDropdownShown
107
+ && evt.parentWidget !== this
108
+ // Don't hide if a submenu wash shown (it might be a submenu of
109
+ // the current menu).
110
+ && evt.parentWidget.toplevel) {
111
+ this.setDropdownVisible(false);
112
+ }
113
+ });
102
114
  }
103
115
  this.setDropdownVisible(false);
104
116
  parent.appendChild(this.container);
@@ -144,6 +156,10 @@ export default class BaseWidget {
144
156
  this.dropdownContainer.classList.remove('hidden');
145
157
  this.container.classList.add('dropdownVisible');
146
158
  this.editor.announceForAccessibility(this.localizationTable.dropdownShown(this.getTitle()));
159
+ this.editor.notifier.dispatch(EditorEventType.ToolbarDropdownShown, {
160
+ kind: EditorEventType.ToolbarDropdownShown,
161
+ parentWidget: this,
162
+ });
147
163
  }
148
164
  else {
149
165
  this.dropdownContainer.classList.add('hidden');
@@ -164,6 +180,10 @@ export default class BaseWidget {
164
180
  this.dropdownContainer.style.transform = '';
165
181
  }
166
182
  }
183
+ /** Set whether the widget is contained within another. @internal */
184
+ setIsToplevel(toplevel) {
185
+ this.toplevel = toplevel;
186
+ }
167
187
  isDropdownVisible() {
168
188
  return !this.dropdownContainer.classList.contains('hidden');
169
189
  }
@@ -16,4 +16,5 @@ export default abstract class BaseTool implements PointerEvtListener {
16
16
  setEnabled(enabled: boolean): void;
17
17
  isEnabled(): boolean;
18
18
  setToolGroup(group: ToolEnabledGroup): void;
19
+ getToolGroup(): ToolEnabledGroup | null;
19
20
  }