js-draw 0.6.0 → 0.7.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 (39) hide show
  1. package/.firebase/hosting.ZG9jcw.cache +338 -0
  2. package/.github/ISSUE_TEMPLATE/translation.md +1 -1
  3. package/CHANGELOG.md +11 -0
  4. package/dist/bundle.js +1 -1
  5. package/dist/src/Editor.d.ts +0 -1
  6. package/dist/src/Editor.js +4 -3
  7. package/dist/src/SVGLoader.js +2 -2
  8. package/dist/src/components/Stroke.js +1 -0
  9. package/dist/src/components/Text.d.ts +10 -5
  10. package/dist/src/components/Text.js +49 -15
  11. package/dist/src/components/builders/FreehandLineBuilder.d.ts +9 -2
  12. package/dist/src/components/builders/FreehandLineBuilder.js +127 -28
  13. package/dist/src/components/lib.d.ts +2 -2
  14. package/dist/src/components/lib.js +2 -2
  15. package/dist/src/rendering/renderers/CanvasRenderer.js +2 -2
  16. package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -0
  17. package/dist/src/rendering/renderers/SVGRenderer.js +49 -22
  18. package/dist/src/toolbar/IconProvider.d.ts +24 -18
  19. package/dist/src/toolbar/IconProvider.js +23 -21
  20. package/dist/src/toolbar/widgets/PenToolWidget.js +8 -5
  21. package/dist/src/tools/PasteHandler.js +2 -22
  22. package/dist/src/tools/TextTool.d.ts +4 -0
  23. package/dist/src/tools/TextTool.js +76 -15
  24. package/package.json +1 -1
  25. package/src/Editor.toSVG.test.ts +27 -0
  26. package/src/Editor.ts +4 -4
  27. package/src/SVGLoader.test.ts +20 -0
  28. package/src/SVGLoader.ts +4 -4
  29. package/src/components/Stroke.ts +1 -0
  30. package/src/components/Text.test.ts +3 -3
  31. package/src/components/Text.ts +62 -19
  32. package/src/components/builders/FreehandLineBuilder.ts +160 -32
  33. package/src/components/lib.ts +3 -3
  34. package/src/rendering/renderers/CanvasRenderer.ts +2 -2
  35. package/src/rendering/renderers/SVGRenderer.ts +50 -24
  36. package/src/toolbar/IconProvider.ts +24 -20
  37. package/src/toolbar/widgets/PenToolWidget.ts +9 -5
  38. package/src/tools/PasteHandler.ts +2 -24
  39. package/src/tools/TextTool.ts +86 -17
@@ -93,7 +93,6 @@ export declare class Editor {
93
93
  readonly toolController: ToolController;
94
94
  /**
95
95
  * Global event dispatcher/subscriber.
96
- * @see {@link types.EditorEventType}
97
96
  */
98
97
  readonly notifier: EditorNotifier;
99
98
  private loadingWarning;
@@ -42,6 +42,7 @@ import Pointer from './Pointer';
42
42
  import Mat33 from './math/Mat33';
43
43
  import getLocalizationTable from './localizations/getLocalizationTable';
44
44
  import IconProvider from './toolbar/IconProvider';
45
+ import { toRoundedString } from './math/rounding';
45
46
  // { @inheritDoc Editor! }
46
47
  export class Editor {
47
48
  /**
@@ -638,9 +639,9 @@ export class Editor {
638
639
  importExportViewport.resetTransform(origTransform);
639
640
  // Just show the main region
640
641
  const rect = importExportViewport.visibleRect;
641
- result.setAttribute('viewBox', `${rect.x} ${rect.y} ${rect.w} ${rect.h}`);
642
- result.setAttribute('width', `${rect.w}`);
643
- result.setAttribute('height', `${rect.h}`);
642
+ result.setAttribute('viewBox', [rect.x, rect.y, rect.w, rect.h].map(part => toRoundedString(part)).join(' '));
643
+ result.setAttribute('width', toRoundedString(rect.w));
644
+ result.setAttribute('height', toRoundedString(rect.h));
644
645
  // Ensure the image can be identified as an SVG if downloaded.
645
646
  // See https://jwatt.org/svg/authoring/
646
647
  result.setAttribute('version', '1.1');
@@ -11,7 +11,7 @@ import Color4 from './Color4';
11
11
  import ImageComponent from './components/ImageComponent';
12
12
  import Stroke from './components/Stroke';
13
13
  import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
14
- import Text from './components/Text';
14
+ import TextComponent from './components/Text';
15
15
  import UnknownSVGObject from './components/UnknownSVGObject';
16
16
  import Mat33 from './math/Mat33';
17
17
  import Path from './math/Path';
@@ -209,7 +209,7 @@ export default class SVGLoader {
209
209
  };
210
210
  const supportedAttrs = [];
211
211
  const transform = this.getTransform(elem, supportedAttrs, computedStyles);
212
- const result = new Text(contentList, transform, style);
212
+ const result = new TextComponent(contentList, transform, style);
213
213
  this.attachUnrecognisedAttrs(result, elem, new Set(supportedAttrs), new Set(supportedStyleAttrs));
214
214
  return result;
215
215
  }
@@ -3,6 +3,7 @@ import Rect2 from '../math/Rect2';
3
3
  import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
4
4
  import AbstractComponent from './AbstractComponent';
5
5
  export default class Stroke extends AbstractComponent {
6
+ // Creates a `Stroke` from the given `parts`.
6
7
  constructor(parts) {
7
8
  var _a;
8
9
  super('stroke');
@@ -12,24 +12,29 @@ export interface TextStyle {
12
12
  fontVariant?: string;
13
13
  renderingStyle: RenderingStyle;
14
14
  }
15
- export default class Text extends AbstractComponent {
16
- protected readonly textObjects: Array<string | Text>;
15
+ export default class TextComponent extends AbstractComponent {
16
+ protected readonly textObjects: Array<string | TextComponent>;
17
17
  private transform;
18
- private readonly style;
18
+ private style;
19
19
  protected contentBBox: Rect2;
20
- constructor(textObjects: Array<string | Text>, transform: Mat33, style: TextStyle);
20
+ constructor(textObjects: Array<string | TextComponent>, transform: Mat33, style: TextStyle);
21
21
  static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextStyle): void;
22
22
  private static textMeasuringCtx;
23
23
  private static estimateTextDimens;
24
24
  private static getTextDimens;
25
25
  private computeBBoxOfPart;
26
26
  private recomputeBBox;
27
+ private renderInternal;
27
28
  render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
28
29
  intersects(lineSegment: LineSegment2): boolean;
30
+ getBaselinePos(): import("../lib").Vec3;
31
+ getTextStyle(): TextStyle;
32
+ getTransform(): Mat33;
29
33
  protected applyTransformation(affineTransfm: Mat33): void;
30
34
  protected createClone(): AbstractComponent;
31
35
  getText(): string;
32
36
  description(localizationTable: ImageComponentLocalization): string;
33
37
  protected serializeToJSON(): Record<string, any>;
34
- static deserializeFromString(json: any): Text;
38
+ static deserializeFromString(json: any): TextComponent;
39
+ static fromLines(lines: string[], transform: Mat33, style: TextStyle): AbstractComponent;
35
40
  }
@@ -1,16 +1,23 @@
1
1
  import LineSegment2 from '../math/LineSegment2';
2
2
  import Mat33 from '../math/Mat33';
3
3
  import Rect2 from '../math/Rect2';
4
+ import { Vec2 } from '../math/Vec2';
4
5
  import { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
5
6
  import AbstractComponent from './AbstractComponent';
6
7
  const componentTypeId = 'text';
7
- export default class Text extends AbstractComponent {
8
+ export default class TextComponent extends AbstractComponent {
8
9
  constructor(textObjects, transform, style) {
9
10
  super(componentTypeId);
10
11
  this.textObjects = textObjects;
11
12
  this.transform = transform;
12
13
  this.style = style;
13
14
  this.recomputeBBox();
15
+ // If this has no direct children, choose a style representative of this' content
16
+ // (useful for estimating the style of the TextComponent).
17
+ const hasDirectContent = textObjects.some(obj => typeof obj === 'string');
18
+ if (!hasDirectContent && textObjects.length > 0) {
19
+ this.style = textObjects[0].getTextStyle();
20
+ }
14
21
  }
15
22
  static applyTextStyles(ctx, style) {
16
23
  var _a, _b;
@@ -35,12 +42,12 @@ export default class Text extends AbstractComponent {
35
42
  // Returns the bounding box of `text`. This is approximate if no Canvas is available.
36
43
  static getTextDimens(text, style) {
37
44
  var _a, _b;
38
- (_a = Text.textMeasuringCtx) !== null && _a !== void 0 ? _a : (Text.textMeasuringCtx = (_b = document.createElement('canvas').getContext('2d')) !== null && _b !== void 0 ? _b : null);
39
- if (!Text.textMeasuringCtx) {
45
+ (_a = TextComponent.textMeasuringCtx) !== null && _a !== void 0 ? _a : (TextComponent.textMeasuringCtx = (_b = document.createElement('canvas').getContext('2d')) !== null && _b !== void 0 ? _b : null);
46
+ if (!TextComponent.textMeasuringCtx) {
40
47
  return this.estimateTextDimens(text, style);
41
48
  }
42
- const ctx = Text.textMeasuringCtx;
43
- Text.applyTextStyles(ctx, style);
49
+ const ctx = TextComponent.textMeasuringCtx;
50
+ TextComponent.applyTextStyles(ctx, style);
44
51
  const measure = ctx.measureText(text);
45
52
  // Text is drawn with (0,0) at the bottom left of the baseline.
46
53
  const textY = -measure.actualBoundingBoxAscent;
@@ -49,7 +56,7 @@ export default class Text extends AbstractComponent {
49
56
  }
50
57
  computeBBoxOfPart(part) {
51
58
  if (typeof part === 'string') {
52
- const textBBox = Text.getTextDimens(part, this.style);
59
+ const textBBox = TextComponent.getTextDimens(part, this.style);
53
60
  return textBBox.transformedBoundingBox(this.transform);
54
61
  }
55
62
  else {
@@ -66,19 +73,22 @@ export default class Text extends AbstractComponent {
66
73
  }
67
74
  this.contentBBox = bbox !== null && bbox !== void 0 ? bbox : Rect2.empty;
68
75
  }
69
- render(canvas, _visibleRect) {
76
+ renderInternal(canvas) {
70
77
  const cursor = this.transform;
71
- canvas.startObject(this.contentBBox);
72
78
  for (const textObject of this.textObjects) {
73
79
  if (typeof textObject === 'string') {
74
80
  canvas.drawText(textObject, cursor, this.style);
75
81
  }
76
82
  else {
77
83
  canvas.pushTransform(cursor);
78
- textObject.render(canvas);
84
+ textObject.renderInternal(canvas);
79
85
  canvas.popTransform();
80
86
  }
81
87
  }
88
+ }
89
+ render(canvas, _visibleRect) {
90
+ canvas.startObject(this.contentBBox);
91
+ this.renderInternal(canvas);
82
92
  canvas.endObject(this.getLoadSaveData());
83
93
  }
84
94
  intersects(lineSegment) {
@@ -89,7 +99,7 @@ export default class Text extends AbstractComponent {
89
99
  lineSegment = new LineSegment2(p1InThisSpace, p2InThisSpace);
90
100
  for (const subObject of this.textObjects) {
91
101
  if (typeof subObject === 'string') {
92
- const textBBox = Text.getTextDimens(subObject, this.style);
102
+ const textBBox = TextComponent.getTextDimens(subObject, this.style);
93
103
  // TODO: Use a better intersection check. Perhaps draw the text onto a CanvasElement and
94
104
  // use pixel-testing to check for intersection with its contour.
95
105
  if (textBBox.getEdges().some(edge => lineSegment.intersection(edge) !== null)) {
@@ -104,12 +114,21 @@ export default class Text extends AbstractComponent {
104
114
  }
105
115
  return false;
106
116
  }
117
+ getBaselinePos() {
118
+ return this.transform.transformVec2(Vec2.zero);
119
+ }
120
+ getTextStyle() {
121
+ return this.style;
122
+ }
123
+ getTransform() {
124
+ return this.transform;
125
+ }
107
126
  applyTransformation(affineTransfm) {
108
127
  this.transform = affineTransfm.rightMul(this.transform);
109
128
  this.recomputeBBox();
110
129
  }
111
130
  createClone() {
112
- return new Text(this.textObjects, this.transform, this.style);
131
+ return new TextComponent(this.textObjects, this.transform, this.style);
113
132
  }
114
133
  getText() {
115
134
  const result = [];
@@ -159,7 +178,7 @@ export default class Text extends AbstractComponent {
159
178
  if (((_a = data.text) !== null && _a !== void 0 ? _a : null) !== null) {
160
179
  return data.text;
161
180
  }
162
- return Text.deserializeFromString(data.json);
181
+ return TextComponent.deserializeFromString(data.json);
163
182
  });
164
183
  json.transform = json.transform.filter((elem) => typeof elem === 'number');
165
184
  if (json.transform.length !== 9) {
@@ -167,8 +186,23 @@ export default class Text extends AbstractComponent {
167
186
  }
168
187
  const transformData = json.transform;
169
188
  const transform = new Mat33(...transformData);
170
- return new Text(textObjects, transform, style);
189
+ return new TextComponent(textObjects, transform, style);
190
+ }
191
+ static fromLines(lines, transform, style) {
192
+ let lastComponent = null;
193
+ const components = [];
194
+ for (const line of lines) {
195
+ let position = Vec2.zero;
196
+ if (lastComponent) {
197
+ const lineMargin = Math.floor(style.size);
198
+ position = lastComponent.getBBox().bottomLeft.plus(Vec2.unitY.times(lineMargin));
199
+ }
200
+ const component = new TextComponent([line], Mat33.translation(position), style);
201
+ components.push(component);
202
+ lastComponent = component;
203
+ }
204
+ return new TextComponent(components, transform, style);
171
205
  }
172
206
  }
173
- Text.textMeasuringCtx = null;
174
- AbstractComponent.registerComponent(componentTypeId, (data) => Text.deserializeFromString(data));
207
+ TextComponent.textMeasuringCtx = null;
208
+ AbstractComponent.registerComponent(componentTypeId, (data) => TextComponent.deserializeFromString(data));
@@ -1,6 +1,7 @@
1
1
  import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
2
2
  import Rect2 from '../../math/Rect2';
3
3
  import Stroke from '../Stroke';
4
+ import Viewport from '../../Viewport';
4
5
  import { StrokeDataPoint } from '../../types';
5
6
  import { ComponentBuilder, ComponentBuilderFactory } from './types';
6
7
  export declare const makeFreehandLineBuilder: ComponentBuilderFactory;
@@ -8,11 +9,15 @@ export default class FreehandLineBuilder implements ComponentBuilder {
8
9
  private startPoint;
9
10
  private minFitAllowed;
10
11
  private maxFitAllowed;
12
+ private viewport;
11
13
  private isFirstSegment;
12
14
  private pathStartConnector;
13
15
  private mostRecentConnector;
14
16
  private upperSegments;
15
17
  private lowerSegments;
18
+ private lastUpperBezier;
19
+ private lastLowerBezier;
20
+ private parts;
16
21
  private buffer;
17
22
  private lastPoint;
18
23
  private lastExitingVec;
@@ -21,14 +26,16 @@ export default class FreehandLineBuilder implements ComponentBuilder {
21
26
  private curveEndWidth;
22
27
  private momentum;
23
28
  private bbox;
24
- constructor(startPoint: StrokeDataPoint, minFitAllowed: number, maxFitAllowed: number);
29
+ constructor(startPoint: StrokeDataPoint, minFitAllowed: number, maxFitAllowed: number, viewport: Viewport);
25
30
  getBBox(): Rect2;
26
31
  private getRenderingStyle;
27
- private previewPath;
32
+ private previewCurrentPath;
33
+ private previewFullPath;
28
34
  private previewStroke;
29
35
  preview(renderer: AbstractRenderer): void;
30
36
  build(): Stroke;
31
37
  private roundPoint;
38
+ private shouldStartNewSegment;
32
39
  private approxCurrentCurveLength;
33
40
  private finalizeCurrentCurve;
34
41
  private currentSegmentToPath;
@@ -6,11 +6,11 @@ import LineSegment2 from '../../math/LineSegment2';
6
6
  import Stroke from '../Stroke';
7
7
  import Viewport from '../../Viewport';
8
8
  export const makeFreehandLineBuilder = (initialPoint, viewport) => {
9
- // Don't smooth if input is more than ± 7 pixels from the true curve, do smooth if
9
+ // Don't smooth if input is more than ± 3 pixels from the true curve, do smooth if
10
10
  // less than ±1 px from the curve.
11
- const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 7;
11
+ const maxSmoothingDist = viewport.getSizeOfPixelOnCanvas() * 3;
12
12
  const minSmoothingDist = viewport.getSizeOfPixelOnCanvas();
13
- return new FreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist);
13
+ return new FreehandLineBuilder(initialPoint, minSmoothingDist, maxSmoothingDist, viewport);
14
14
  };
15
15
  // Handles stroke smoothing and creates Strokes from user/stylus input.
16
16
  export default class FreehandLineBuilder {
@@ -19,13 +19,17 @@ export default class FreehandLineBuilder {
19
19
  // for which a point is considered 'part of the curve'.
20
20
  // Note that the maximum will be smaller if the stroke width is less than
21
21
  // [maxFitAllowed].
22
- minFitAllowed, maxFitAllowed) {
22
+ minFitAllowed, maxFitAllowed, viewport) {
23
23
  this.startPoint = startPoint;
24
24
  this.minFitAllowed = minFitAllowed;
25
25
  this.maxFitAllowed = maxFitAllowed;
26
+ this.viewport = viewport;
26
27
  this.isFirstSegment = true;
27
28
  this.pathStartConnector = null;
28
29
  this.mostRecentConnector = null;
30
+ this.lastUpperBezier = null;
31
+ this.lastLowerBezier = null;
32
+ this.parts = [];
29
33
  this.lastExitingVec = null;
30
34
  this.currentCurve = null;
31
35
  this.lastPoint = this.startPoint;
@@ -46,16 +50,16 @@ export default class FreehandLineBuilder {
46
50
  fill: (_a = this.lastPoint.color) !== null && _a !== void 0 ? _a : null,
47
51
  };
48
52
  }
49
- previewPath() {
53
+ previewCurrentPath() {
50
54
  var _a;
51
- let upperPath;
52
- let lowerPath;
55
+ const upperPath = this.upperSegments.slice();
56
+ const lowerPath = this.lowerSegments.slice();
53
57
  let lowerToUpperCap;
54
58
  let pathStartConnector;
55
59
  if (this.currentCurve) {
56
- const { upperCurve, lowerToUpperConnector, upperToLowerConnector, lowerCurve } = this.currentSegmentToPath();
57
- upperPath = this.upperSegments.concat(upperCurve);
58
- lowerPath = this.lowerSegments.concat(lowerCurve);
60
+ const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand } = this.currentSegmentToPath();
61
+ upperPath.push(upperCurveCommand);
62
+ lowerPath.push(lowerCurveCommand);
59
63
  lowerToUpperCap = lowerToUpperConnector;
60
64
  pathStartConnector = (_a = this.pathStartConnector) !== null && _a !== void 0 ? _a : upperToLowerConnector;
61
65
  }
@@ -63,12 +67,17 @@ export default class FreehandLineBuilder {
63
67
  if (this.mostRecentConnector === null || this.pathStartConnector === null) {
64
68
  return null;
65
69
  }
66
- upperPath = this.upperSegments.slice();
67
- lowerPath = this.lowerSegments.slice();
68
70
  lowerToUpperCap = this.mostRecentConnector;
69
71
  pathStartConnector = this.pathStartConnector;
70
72
  }
71
- const startPoint = lowerPath[lowerPath.length - 1].endPoint;
73
+ let startPoint;
74
+ const lastLowerSegment = lowerPath[lowerPath.length - 1];
75
+ if (lastLowerSegment.kind === PathCommandType.LineTo || lastLowerSegment.kind === PathCommandType.MoveTo) {
76
+ startPoint = lastLowerSegment.point;
77
+ }
78
+ else {
79
+ startPoint = lastLowerSegment.endPoint;
80
+ }
72
81
  return {
73
82
  // Start at the end of the lower curve:
74
83
  // Start point
@@ -103,21 +112,33 @@ export default class FreehandLineBuilder {
103
112
  style: this.getRenderingStyle(),
104
113
  };
105
114
  }
115
+ previewFullPath() {
116
+ const preview = this.previewCurrentPath();
117
+ if (preview) {
118
+ return [...this.parts, preview];
119
+ }
120
+ return null;
121
+ }
106
122
  previewStroke() {
107
- const pathPreview = this.previewPath();
123
+ const pathPreview = this.previewFullPath();
108
124
  if (pathPreview) {
109
- return new Stroke([pathPreview]);
125
+ return new Stroke(pathPreview);
110
126
  }
111
127
  return null;
112
128
  }
113
129
  preview(renderer) {
114
- const path = this.previewPath();
115
- if (path) {
116
- renderer.drawPath(path);
130
+ const paths = this.previewFullPath();
131
+ if (paths) {
132
+ const approxBBox = this.viewport.visibleRect;
133
+ renderer.startObject(approxBBox);
134
+ for (const path of paths) {
135
+ renderer.drawPath(path);
136
+ }
137
+ renderer.endObject();
117
138
  }
118
139
  }
119
140
  build() {
120
- if (this.lastPoint && (this.lowerSegments.length === 0 || this.approxCurrentCurveLength() > this.curveStartWidth * 2)) {
141
+ if (this.lastPoint) {
121
142
  this.finalizeCurrentCurve();
122
143
  }
123
144
  return this.previewStroke();
@@ -129,6 +150,61 @@ export default class FreehandLineBuilder {
129
150
  }
130
151
  return Viewport.roundPoint(point, minFit);
131
152
  }
153
+ // Returns true if, due to overlap with previous segments, a new RenderablePathSpec should be created.
154
+ shouldStartNewSegment(lowerCurve, upperCurve) {
155
+ if (!this.lastLowerBezier || !this.lastUpperBezier) {
156
+ return false;
157
+ }
158
+ const getIntersection = (curve1, curve2) => {
159
+ const intersection = curve1.intersects(curve2);
160
+ if (!intersection || intersection.length === 0) {
161
+ return null;
162
+ }
163
+ // From http://pomax.github.io/bezierjs/#intersect-curve,
164
+ // .intersects returns an array of 't1/t2' pairs, where curve1.at(t1) gives the point.
165
+ const firstTPair = intersection[0];
166
+ const match = /^([-0-9.eE]+)\/([-0-9.eE]+)$/.exec(firstTPair);
167
+ if (!match) {
168
+ throw new Error(`Incorrect format returned by .intersects: ${intersection} should be array of "number/number"!`);
169
+ }
170
+ const t = parseFloat(match[1]);
171
+ return Vec2.ofXY(curve1.get(t));
172
+ };
173
+ const getExitDirection = (curve) => {
174
+ return Vec2.ofXY(curve.points[2]).minus(Vec2.ofXY(curve.points[1])).normalized();
175
+ };
176
+ const getEnterDirection = (curve) => {
177
+ return Vec2.ofXY(curve.points[1]).minus(Vec2.ofXY(curve.points[0])).normalized();
178
+ };
179
+ // Prevent
180
+ // /
181
+ // / /
182
+ // / / /|
183
+ // / / |
184
+ // / |
185
+ // where the next stroke and the previous stroke are in different directions.
186
+ //
187
+ // Are the exit/enter directions of the previous and current curves in different enough directions?
188
+ if (getEnterDirection(upperCurve).dot(getExitDirection(this.lastUpperBezier)) < 0.3
189
+ || getEnterDirection(lowerCurve).dot(getExitDirection(this.lastLowerBezier)) < 0.3
190
+ // Also handle if the curves exit/enter directions differ
191
+ || getEnterDirection(upperCurve).dot(getExitDirection(upperCurve)) < 0
192
+ || getEnterDirection(lowerCurve).dot(getExitDirection(lowerCurve)) < 0) {
193
+ return true;
194
+ }
195
+ // Check whether the lower curve intersects the other wall:
196
+ // / / ← lower
197
+ // / / /
198
+ // / / /
199
+ // //
200
+ // / /
201
+ const lowerIntersection = getIntersection(lowerCurve, this.lastUpperBezier);
202
+ const upperIntersection = getIntersection(upperCurve, this.lastLowerBezier);
203
+ if (lowerIntersection || upperIntersection) {
204
+ return true;
205
+ }
206
+ return false;
207
+ }
132
208
  // Returns the distance between the start, control, and end points of the curve.
133
209
  approxCurrentCurveLength() {
134
210
  if (!this.currentCurve) {
@@ -185,8 +261,17 @@ export default class FreehandLineBuilder {
185
261
  this.mostRecentConnector = this.pathStartConnector;
186
262
  return;
187
263
  }
188
- const { upperCurve, lowerToUpperConnector, upperToLowerConnector, lowerCurve } = this.currentSegmentToPath();
189
- if (this.isFirstSegment) {
264
+ const { upperCurveCommand, lowerToUpperConnector, upperToLowerConnector, lowerCurveCommand, lowerCurve, upperCurve, } = this.currentSegmentToPath();
265
+ const shouldStartNew = this.shouldStartNewSegment(lowerCurve, upperCurve);
266
+ if (shouldStartNew) {
267
+ const part = this.previewCurrentPath();
268
+ if (part) {
269
+ this.parts.push(part);
270
+ this.upperSegments = [];
271
+ this.lowerSegments = [];
272
+ }
273
+ }
274
+ if (this.isFirstSegment || shouldStartNew) {
190
275
  // We draw the upper path (reversed), then the lower path, so we need the
191
276
  // upperToLowerConnector to join the two paths.
192
277
  this.pathStartConnector = upperToLowerConnector;
@@ -195,8 +280,10 @@ export default class FreehandLineBuilder {
195
280
  // With the most recent connector, we're joining the end of the lowerPath to the most recent
196
281
  // upperPath:
197
282
  this.mostRecentConnector = lowerToUpperConnector;
198
- this.upperSegments.push(upperCurve);
199
- this.lowerSegments.push(lowerCurve);
283
+ this.lowerSegments.push(lowerCurveCommand);
284
+ this.upperSegments.push(upperCurveCommand);
285
+ this.lastLowerBezier = lowerCurve;
286
+ this.lastUpperBezier = upperCurve;
200
287
  const lastPoint = this.buffer[this.buffer.length - 1];
201
288
  this.lastExitingVec = Vec2.ofXY(this.currentCurve.points[2]).minus(Vec2.ofXY(this.currentCurve.points[1]));
202
289
  console.assert(this.lastExitingVec.magnitude() !== 0, 'lastExitingVec has zero length!');
@@ -238,11 +325,13 @@ export default class FreehandLineBuilder {
238
325
  .normalized().times(this.curveStartWidth / 2 * halfVecT
239
326
  + this.curveEndWidth / 2 * (1 - halfVecT));
240
327
  // Each starts at startPt ± startVec
328
+ const lowerCurveStartPoint = this.roundPoint(startPt.plus(startVec));
241
329
  const lowerCurveControlPoint = this.roundPoint(controlPoint.plus(halfVec));
242
330
  const lowerCurveEndPoint = this.roundPoint(endPt.plus(endVec));
243
331
  const upperCurveControlPoint = this.roundPoint(controlPoint.minus(halfVec));
244
332
  const upperCurveStartPoint = this.roundPoint(endPt.minus(endVec));
245
- const lowerCurve = {
333
+ const upperCurveEndPoint = this.roundPoint(startPt.minus(startVec));
334
+ const lowerCurveCommand = {
246
335
  kind: PathCommandType.QuadraticBezierTo,
247
336
  controlPoint: lowerCurveControlPoint,
248
337
  endPoint: lowerCurveEndPoint,
@@ -250,19 +339,24 @@ export default class FreehandLineBuilder {
250
339
  // From the end of the upperCurve to the start of the lowerCurve:
251
340
  const upperToLowerConnector = {
252
341
  kind: PathCommandType.LineTo,
253
- point: this.roundPoint(startPt.plus(startVec)),
342
+ point: lowerCurveStartPoint,
254
343
  };
255
344
  // From the end of lowerCurve to the start of upperCurve:
256
345
  const lowerToUpperConnector = {
257
346
  kind: PathCommandType.LineTo,
258
347
  point: upperCurveStartPoint,
259
348
  };
260
- const upperCurve = {
349
+ const upperCurveCommand = {
261
350
  kind: PathCommandType.QuadraticBezierTo,
262
351
  controlPoint: upperCurveControlPoint,
263
- endPoint: this.roundPoint(startPt.minus(startVec)),
352
+ endPoint: upperCurveEndPoint,
353
+ };
354
+ const upperCurve = new Bezier(upperCurveStartPoint, upperCurveControlPoint, upperCurveEndPoint);
355
+ const lowerCurve = new Bezier(lowerCurveStartPoint, lowerCurveControlPoint, lowerCurveEndPoint);
356
+ return {
357
+ upperCurveCommand, upperToLowerConnector, lowerToUpperConnector, lowerCurveCommand,
358
+ upperCurve, lowerCurve,
264
359
  };
265
- return { upperCurve, upperToLowerConnector, lowerToUpperConnector, lowerCurve };
266
360
  }
267
361
  // Compute the direction of the velocity at the end of this.buffer
268
362
  computeExitingVec() {
@@ -299,6 +393,10 @@ export default class FreehandLineBuilder {
299
393
  const pointRadius = newPoint.width / 2;
300
394
  const prevEndWidth = this.curveEndWidth;
301
395
  this.curveEndWidth = pointRadius;
396
+ if (this.isFirstSegment) {
397
+ // The start of a curve often lacks accurate pressure information. Update it.
398
+ this.curveStartWidth = (this.curveStartWidth + pointRadius) / 2;
399
+ }
302
400
  // recompute bbox
303
401
  this.bbox = this.bbox.grownToPoint(newPoint.pos, pointRadius);
304
402
  if (this.currentCurve === null) {
@@ -310,6 +408,7 @@ export default class FreehandLineBuilder {
310
408
  this.curveStartWidth = lastPoint.width / 2;
311
409
  console.assert(!isNaN(p1.magnitude()) && !isNaN(p2.magnitude()) && !isNaN(p3.magnitude()), 'Expected !NaN');
312
410
  }
411
+ // If there isn't an entering vector (e.g. because this.isFirstCurve), approximate it.
313
412
  let enteringVec = this.lastExitingVec;
314
413
  if (!enteringVec) {
315
414
  let sampleIdx = Math.ceil(this.buffer.length / 2);
@@ -3,6 +3,6 @@ export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
3
3
  export * from './AbstractComponent';
4
4
  export { default as AbstractComponent } from './AbstractComponent';
5
5
  import Stroke from './Stroke';
6
- import Text from './Text';
6
+ import TextComponent from './Text';
7
7
  import ImageComponent from './ImageComponent';
8
- export { Stroke, Text, Text as TextComponent, Stroke as StrokeComponent, ImageComponent, };
8
+ export { Stroke, TextComponent as Text, TextComponent as TextComponent, Stroke as StrokeComponent, ImageComponent, };
@@ -3,6 +3,6 @@ export { makeFreehandLineBuilder } from './builders/FreehandLineBuilder';
3
3
  export * from './AbstractComponent';
4
4
  export { default as AbstractComponent } from './AbstractComponent';
5
5
  import Stroke from './Stroke';
6
- import Text from './Text';
6
+ import TextComponent from './Text';
7
7
  import ImageComponent from './ImageComponent';
8
- export { Stroke, Text, Text as TextComponent, Stroke as StrokeComponent, ImageComponent, };
8
+ export { Stroke, TextComponent as Text, TextComponent as TextComponent, Stroke as StrokeComponent, ImageComponent, };
@@ -1,5 +1,5 @@
1
1
  import Color4 from '../../Color4';
2
- import Text from '../../components/Text';
2
+ import TextComponent from '../../components/Text';
3
3
  import { Vec2 } from '../../math/Vec2';
4
4
  import AbstractRenderer from './AbstractRenderer';
5
5
  export default class CanvasRenderer extends AbstractRenderer {
@@ -113,7 +113,7 @@ export default class CanvasRenderer extends AbstractRenderer {
113
113
  this.ctx.save();
114
114
  transform = this.getCanvasToScreenTransform().rightMul(transform);
115
115
  this.transformBy(transform);
116
- Text.applyTextStyles(this.ctx, style);
116
+ TextComponent.applyTextStyles(this.ctx, style);
117
117
  if (style.renderingStyle.fill.a !== 0) {
118
118
  this.ctx.fillStyle = style.renderingStyle.fill.toHexString();
119
119
  this.ctx.fillText(text, 0, 0);
@@ -20,6 +20,8 @@ export default class SVGRenderer extends AbstractRenderer {
20
20
  private addPathToSVG;
21
21
  drawPath(pathSpec: RenderablePathSpec): void;
22
22
  private transformFrom;
23
+ private textContainer;
24
+ private textContainerTransform;
23
25
  drawText(text: string, transform: Mat33, style: TextStyle): void;
24
26
  drawImage(image: RenderableImage): void;
25
27
  startObject(boundingBox: Rect2): void;