js-draw 0.4.0 → 0.4.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 (45) hide show
  1. package/.github/pull_request_template.md +15 -0
  2. package/.github/workflows/firebase-hosting-merge.yml +7 -0
  3. package/.github/workflows/firebase-hosting-pull-request.yml +10 -0
  4. package/.github/workflows/github-pages.yml +2 -0
  5. package/CHANGELOG.md +7 -0
  6. package/dist/bundle.js +1 -1
  7. package/dist/src/Editor.js +3 -1
  8. package/dist/src/components/AbstractComponent.js +1 -0
  9. package/dist/src/components/Stroke.js +15 -9
  10. package/dist/src/components/Text.d.ts +1 -1
  11. package/dist/src/components/Text.js +1 -1
  12. package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -0
  13. package/dist/src/components/builders/FreehandLineBuilder.js +34 -36
  14. package/dist/src/math/Vec3.d.ts +1 -1
  15. package/dist/src/math/Vec3.js +1 -1
  16. package/dist/src/testing/beforeEachFile.d.ts +1 -0
  17. package/dist/src/testing/beforeEachFile.js +3 -0
  18. package/dist/src/testing/createEditor.d.ts +1 -0
  19. package/dist/src/testing/createEditor.js +7 -1
  20. package/dist/src/testing/loadExpectExtensions.d.ts +0 -15
  21. package/dist/src/tools/SelectionTool/SelectionTool.js +8 -0
  22. package/jest.config.js +5 -0
  23. package/package.json +15 -14
  24. package/src/Editor.ts +2 -0
  25. package/src/components/AbstractComponent.ts +2 -0
  26. package/src/components/Stroke.test.ts +0 -3
  27. package/src/components/Stroke.ts +14 -7
  28. package/src/components/Text.test.ts +0 -3
  29. package/src/components/Text.ts +2 -2
  30. package/src/components/builders/FreehandLineBuilder.ts +37 -43
  31. package/src/language/assertions.ts +2 -2
  32. package/src/math/LineSegment2.test.ts +8 -10
  33. package/src/math/Mat33.test.ts +0 -2
  34. package/src/math/Rect2.test.ts +0 -3
  35. package/src/math/Vec2.test.ts +0 -3
  36. package/src/math/Vec3.test.ts +0 -3
  37. package/src/math/Vec3.ts +1 -1
  38. package/src/testing/beforeEachFile.ts +3 -0
  39. package/src/testing/createEditor.ts +8 -1
  40. package/src/testing/global.d.ts +17 -0
  41. package/src/testing/loadExpectExtensions.ts +0 -15
  42. package/src/toolbar/toolbar.css +3 -2
  43. package/src/tools/Pen.test.ts +150 -0
  44. package/src/tools/SelectionTool/SelectionTool.ts +9 -0
  45. package/tsconfig.json +3 -1
@@ -607,7 +607,9 @@ export class Editor {
607
607
  }
608
608
  // Dispatch a pen event to the currently selected tool.
609
609
  // Intended primarially for unit tests.
610
- sendPenEvent(eventType, point, allPointers) {
610
+ sendPenEvent(eventType, point,
611
+ // @deprecated
612
+ allPointers) {
611
613
  const mainPointer = Pointer.ofCanvasPoint(point, eventType !== InputEvtType.PointerUpEvt, this.viewport);
612
614
  this.toolController.dispatchInputEvent({
613
615
  kind: eventType,
@@ -48,6 +48,7 @@ export default class AbstractComponent {
48
48
  transformBy(affineTransfm) {
49
49
  return new AbstractComponent.TransformElementCommand(affineTransfm, this);
50
50
  }
51
+ // Returns a copy of this component.
51
52
  clone() {
52
53
  const clone = this.createClone();
53
54
  for (const attachmentKey in this.loadSaveData) {
@@ -6,7 +6,8 @@ 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 = [];
10
+ for (const section of parts) {
10
11
  const path = Path.fromRenderable(section);
11
12
  const pathBBox = this.bboxForPart(path.bbox, section.style);
12
13
  if (!this.contentBBox) {
@@ -15,14 +16,14 @@ export default class Stroke extends AbstractComponent {
15
16
  else {
16
17
  this.contentBBox = this.contentBBox.union(pathBBox);
17
18
  }
18
- return {
19
+ this.parts.push({
19
20
  path,
20
21
  // To implement RenderablePathSpec
21
22
  startPoint: path.startPoint,
22
23
  style: section.style,
23
24
  commands: path.parts,
24
- };
25
- });
25
+ });
26
+ }
26
27
  (_a = this.contentBBox) !== null && _a !== void 0 ? _a : (this.contentBBox = Rect2.empty);
27
28
  }
28
29
  intersects(line) {
@@ -80,11 +81,16 @@ export default class Stroke extends AbstractComponent {
80
81
  });
81
82
  }
82
83
  getPath() {
83
- var _a;
84
- return (_a = this.parts.reduce((accumulator, current) => {
85
- var _a;
86
- return (_a = accumulator === null || accumulator === void 0 ? void 0 : accumulator.union(current.path)) !== null && _a !== void 0 ? _a : current.path;
87
- }, null)) !== null && _a !== void 0 ? _a : Path.empty;
84
+ let result = null;
85
+ for (const part of this.parts) {
86
+ if (result) {
87
+ result = result.union(part.path);
88
+ }
89
+ else {
90
+ result !== null && result !== void 0 ? result : (result = part.path);
91
+ }
92
+ }
93
+ return result !== null && result !== void 0 ? result : Path.empty;
88
94
  }
89
95
  description(localization) {
90
96
  return localization.stroke;
@@ -29,7 +29,7 @@ export default class Text extends AbstractComponent {
29
29
  intersects(lineSegment: LineSegment2): boolean;
30
30
  protected applyTransformation(affineTransfm: Mat33): void;
31
31
  protected createClone(): AbstractComponent;
32
- private getText;
32
+ getText(): string;
33
33
  description(localizationTable: ImageComponentLocalization): string;
34
34
  protected serializeToJSON(): Record<string, any>;
35
35
  static deserializeFromString(json: any, getTextDimens?: GetTextDimensCallback): Text;
@@ -113,7 +113,7 @@ export default class Text extends AbstractComponent {
113
113
  result.push(textObject.getText());
114
114
  }
115
115
  }
116
- return result.join(' ');
116
+ return result.join('\n');
117
117
  }
118
118
  description(localizationTable) {
119
119
  return localizationTable.text(this.getText());
@@ -29,6 +29,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
29
29
  preview(renderer: AbstractRenderer): void;
30
30
  build(): Stroke;
31
31
  private roundPoint;
32
+ private approxCurrentCurveLength;
32
33
  private finalizeCurrentCurve;
33
34
  private currentSegmentToPath;
34
35
  private computeExitingVec;
@@ -117,7 +117,7 @@ export default class FreehandLineBuilder {
117
117
  }
118
118
  }
119
119
  build() {
120
- if (this.lastPoint) {
120
+ if (this.lastPoint && (this.lowerSegments.length === 0 || this.approxCurrentCurveLength() > this.curveStartWidth * 2)) {
121
121
  this.finalizeCurrentCurve();
122
122
  }
123
123
  return this.previewStroke();
@@ -129,6 +129,18 @@ export default class FreehandLineBuilder {
129
129
  }
130
130
  return Viewport.roundPoint(point, minFit);
131
131
  }
132
+ // Returns the distance between the start, control, and end points of the curve.
133
+ approxCurrentCurveLength() {
134
+ if (!this.currentCurve) {
135
+ return 0;
136
+ }
137
+ const startPt = Vec2.ofXY(this.currentCurve.points[0]);
138
+ const controlPt = Vec2.ofXY(this.currentCurve.points[1]);
139
+ const endPt = Vec2.ofXY(this.currentCurve.points[2]);
140
+ const toControlDist = startPt.minus(controlPt).length();
141
+ const toEndDist = endPt.minus(controlPt).length();
142
+ return toControlDist + toEndDist;
143
+ }
132
144
  finalizeCurrentCurve() {
133
145
  // Case where no points have been added
134
146
  if (!this.currentCurve) {
@@ -204,10 +216,8 @@ export default class FreehandLineBuilder {
204
216
  let endVec = Vec2.ofXY(this.currentCurve.normal(1)).normalized();
205
217
  startVec = startVec.times(this.curveStartWidth / 2);
206
218
  endVec = endVec.times(this.curveEndWidth / 2);
207
- if (isNaN(startVec.magnitude())) {
208
- // TODO: This can happen when events are too close together. Find out why and
209
- // fix.
210
- console.error('startVec is NaN', startVec, endVec, this.currentCurve);
219
+ if (!isFinite(startVec.magnitude())) {
220
+ console.error('Warning: startVec is NaN or ∞', startVec, endVec, this.currentCurve);
211
221
  startVec = endVec;
212
222
  }
213
223
  const startPt = Vec2.ofXY(this.currentCurve.get(0));
@@ -224,28 +234,18 @@ export default class FreehandLineBuilder {
224
234
  }
225
235
  }
226
236
  const halfVecT = projectionT;
227
- let halfVec = Vec2.ofXY(this.currentCurve.normal(halfVecT))
237
+ const halfVec = Vec2.ofXY(this.currentCurve.normal(halfVecT))
228
238
  .normalized().times(this.curveStartWidth / 2 * halfVecT
229
239
  + this.curveEndWidth / 2 * (1 - halfVecT));
230
- // Computes a boundary curve. [direction] should be either +1 or -1 (determines the side
231
- // of the center curve to place the boundary).
232
- const computeBoundaryCurve = (direction, halfVec) => {
233
- return new Bezier(startPt.plus(startVec.times(direction)), controlPoint.plus(halfVec.times(direction)), endPt.plus(endVec.times(direction)));
234
- };
235
- const boundariesIntersect = () => {
236
- const upperBoundary = computeBoundaryCurve(1, halfVec);
237
- const lowerBoundary = computeBoundaryCurve(-1, halfVec);
238
- return upperBoundary.intersects(lowerBoundary).length > 0;
239
- };
240
- // If the boundaries have intersections, increasing the half vector's length could fix this.
241
- if (boundariesIntersect()) {
242
- halfVec = halfVec.times(1.1);
243
- }
244
240
  // Each starts at startPt ± startVec
241
+ const lowerCurveControlPoint = this.roundPoint(controlPoint.plus(halfVec));
242
+ const lowerCurveEndPoint = this.roundPoint(endPt.plus(endVec));
243
+ const upperCurveControlPoint = this.roundPoint(controlPoint.minus(halfVec));
244
+ const upperCurveStartPoint = this.roundPoint(endPt.minus(endVec));
245
245
  const lowerCurve = {
246
246
  kind: PathCommandType.QuadraticBezierTo,
247
- controlPoint: this.roundPoint(controlPoint.plus(halfVec)),
248
- endPoint: this.roundPoint(endPt.plus(endVec)),
247
+ controlPoint: lowerCurveControlPoint,
248
+ endPoint: lowerCurveEndPoint,
249
249
  };
250
250
  // From the end of the upperCurve to the start of the lowerCurve:
251
251
  const upperToLowerConnector = {
@@ -255,11 +255,11 @@ export default class FreehandLineBuilder {
255
255
  // From the end of lowerCurve to the start of upperCurve:
256
256
  const lowerToUpperConnector = {
257
257
  kind: PathCommandType.LineTo,
258
- point: this.roundPoint(endPt.minus(endVec))
258
+ point: upperCurveStartPoint,
259
259
  };
260
260
  const upperCurve = {
261
261
  kind: PathCommandType.QuadraticBezierTo,
262
- controlPoint: this.roundPoint(controlPoint.minus(halfVec)),
262
+ controlPoint: upperCurveControlPoint,
263
263
  endPoint: this.roundPoint(startPt.minus(startVec)),
264
264
  };
265
265
  return { upperCurve, upperToLowerConnector, lowerToUpperConnector, lowerCurve };
@@ -275,7 +275,6 @@ export default class FreehandLineBuilder {
275
275
  const fuzzEq = 1e-10;
276
276
  const deltaTime = newPoint.time - this.lastPoint.time;
277
277
  if (newPoint.pos.eq(this.lastPoint.pos, fuzzEq) || deltaTime === 0) {
278
- console.warn('Discarding identical point');
279
278
  return;
280
279
  }
281
280
  else if (isNaN(newPoint.pos.magnitude())) {
@@ -321,35 +320,35 @@ export default class FreehandLineBuilder {
321
320
  }
322
321
  let exitingVec = this.computeExitingVec();
323
322
  // Find the intersection between the entering vector and the exiting vector
324
- const maxRelativeLength = 2;
323
+ const maxRelativeLength = 3;
325
324
  const segmentStart = this.buffer[0];
326
325
  const segmentEnd = newPoint.pos;
327
326
  const startEndDist = segmentEnd.minus(segmentStart).magnitude();
328
327
  const maxControlPointDist = maxRelativeLength * startEndDist;
329
328
  // Exit in cases where we would divide by zero
330
- if (maxControlPointDist === 0 || exitingVec.magnitude() === 0 || isNaN(exitingVec.magnitude())) {
329
+ if (maxControlPointDist === 0 || exitingVec.magnitude() === 0 || !isFinite(exitingVec.magnitude())) {
331
330
  return;
332
331
  }
333
- console.assert(!isNaN(enteringVec.magnitude()));
332
+ console.assert(isFinite(enteringVec.magnitude()), 'Pre-normalized enteringVec has NaN or ∞ magnitude!');
334
333
  enteringVec = enteringVec.normalized();
335
334
  exitingVec = exitingVec.normalized();
336
- console.assert(!isNaN(enteringVec.magnitude()));
335
+ console.assert(isFinite(enteringVec.magnitude()), 'Normalized enteringVec has NaN or ∞ magnitude!');
337
336
  const lineFromStart = new LineSegment2(segmentStart, segmentStart.plus(enteringVec.times(maxControlPointDist)));
338
337
  const lineFromEnd = new LineSegment2(segmentEnd.minus(exitingVec.times(maxControlPointDist)), segmentEnd);
339
338
  const intersection = lineFromEnd.intersection(lineFromStart);
340
339
  // Position the control point at this intersection
341
- let controlPoint;
340
+ let controlPoint = null;
342
341
  if (intersection) {
343
342
  controlPoint = intersection.point;
344
343
  }
345
- else {
344
+ // No intersection or the intersection is one of the end points?
345
+ if (!controlPoint || segmentStart.eq(controlPoint) || segmentEnd.eq(controlPoint)) {
346
346
  // Position the control point closer to the first -- the connecting
347
347
  // segment will be roughly a line.
348
348
  controlPoint = segmentStart.plus(enteringVec.times(startEndDist / 4));
349
349
  }
350
- if (isNaN(controlPoint.magnitude()) || isNaN(segmentStart.magnitude())) {
351
- console.error('controlPoint is NaN', intersection, 'Start:', segmentStart, 'End:', segmentEnd, 'in:', enteringVec, 'out:', exitingVec);
352
- }
350
+ console.assert(!segmentStart.eq(controlPoint, 1e-11), 'Start and control points are equal!');
351
+ console.assert(!controlPoint.eq(segmentEnd, 1e-11), 'Control and end points are equal!');
353
352
  const prevCurve = this.currentCurve;
354
353
  this.currentCurve = new Bezier(segmentStart.xy, controlPoint.xy, segmentEnd.xy);
355
354
  if (isNaN(Vec2.ofXY(this.currentCurve.normal(0)).magnitude())) {
@@ -369,8 +368,7 @@ export default class FreehandLineBuilder {
369
368
  }
370
369
  return true;
371
370
  };
372
- const approxCurveLen = controlPoint.minus(segmentStart).magnitude() + segmentEnd.minus(controlPoint).magnitude();
373
- if (this.buffer.length > 3 && approxCurveLen > this.curveEndWidth / 3) {
371
+ if (this.buffer.length > 3 && this.approxCurrentCurveLength() > this.curveStartWidth) {
374
372
  if (!curveMatchesPoints(this.currentCurve)) {
375
373
  // Use a curve that better fits the points
376
374
  this.currentCurve = prevCurve;
@@ -97,7 +97,7 @@ export default class Vec3 {
97
97
  * Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 2.99); // → false
98
98
  * ```
99
99
  */
100
- eq(other: Vec3, fuzz: number): boolean;
100
+ eq(other: Vec3, fuzz?: number): boolean;
101
101
  toString(): string;
102
102
  static unitX: Vec3;
103
103
  static unitY: Vec3;
@@ -156,7 +156,7 @@ export default class Vec3 {
156
156
  * Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 2.99); // → false
157
157
  * ```
158
158
  */
159
- eq(other, fuzz) {
159
+ eq(other, fuzz = 1e-10) {
160
160
  for (let i = 0; i < 3; i++) {
161
161
  if (Math.abs(other.at(i) - this.at(i)) > fuzz) {
162
162
  return false;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ import loadExpectExtensions from './loadExpectExtensions';
2
+ loadExpectExtensions();
3
+ jest.useFakeTimers();
@@ -1,3 +1,4 @@
1
1
  import Editor from '../Editor';
2
+ /** Creates an editor. Should only be used in test files. */
2
3
  declare const _default: () => Editor;
3
4
  export default _default;
@@ -1,3 +1,9 @@
1
1
  import { RenderingMode } from '../rendering/Display';
2
2
  import Editor from '../Editor';
3
- export default () => new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });
3
+ /** Creates an editor. Should only be used in test files. */
4
+ export default () => {
5
+ if (jest === undefined) {
6
+ throw new Error('Files in the testing/ folder should only be used in tests!');
7
+ }
8
+ return new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });
9
+ };
@@ -1,17 +1,2 @@
1
1
  export declare const loadExpectExtensions: () => void;
2
- export interface CustomMatchers<R = unknown> {
3
- objEq(expected: {
4
- eq: (other: any, ...args: any) => boolean;
5
- }, ...opts: any): R;
6
- }
7
- declare global {
8
- export namespace jest {
9
- interface Expect extends CustomMatchers {
10
- }
11
- interface Matchers<R> extends CustomMatchers<R> {
12
- }
13
- interface AsyncAsymmetricMatchers extends CustomMatchers {
14
- }
15
- }
16
- }
17
2
  export default loadExpectExtensions;
@@ -8,6 +8,7 @@ import Viewport from '../../Viewport';
8
8
  import BaseTool from '../BaseTool';
9
9
  import SVGRenderer from '../../rendering/renderers/SVGRenderer';
10
10
  import Selection from './Selection';
11
+ import TextComponent from '../../components/Text';
11
12
  export const cssPrefix = 'selection-tool-';
12
13
  // {@inheritDoc SelectionTool!}
13
14
  export default class SelectionTool extends BaseTool {
@@ -215,10 +216,17 @@ export default class SelectionTool extends BaseTool {
215
216
  const exportElem = document.createElementNS(svgNameSpace, 'svg');
216
217
  const sanitize = true;
217
218
  const renderer = new SVGRenderer(exportElem, exportViewport, sanitize);
219
+ const text = [];
218
220
  for (const elem of selectedElems) {
219
221
  elem.render(renderer);
222
+ if (elem instanceof TextComponent) {
223
+ text.push(elem.getText());
224
+ }
220
225
  }
221
226
  event.setData('image/svg+xml', exportElem.outerHTML);
227
+ if (text.length > 0) {
228
+ event.setData('text/plain', text.join('\n'));
229
+ }
222
230
  return true;
223
231
  }
224
232
  setEnabled(enabled) {
package/jest.config.js CHANGED
@@ -10,6 +10,10 @@ const config = {
10
10
  'js',
11
11
  ],
12
12
 
13
+ testPathIgnorePatterns: [
14
+ '<rootDir>/dist/', '<rootDir>/node_modules/'
15
+ ],
16
+
13
17
  // Mocks.
14
18
  // See https://jestjs.io/docs/webpack#handling-static-assets
15
19
  moduleNameMapper: {
@@ -19,6 +23,7 @@ const config = {
19
23
  },
20
24
 
21
25
  testEnvironment: 'jsdom',
26
+ setupFilesAfterEnv: [ '<rootDir>/src/testing/beforeEachFile.ts' ],
22
27
  };
23
28
 
24
29
  module.exports = config;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "main": "./dist/src/lib.d.ts",
6
6
  "types": "./dist/src/lib.js",
@@ -71,36 +71,37 @@
71
71
  "lint": "eslint .",
72
72
  "linter-precommit": "eslint --fix --ext .js --ext .ts",
73
73
  "lint-staged": "lint-staged",
74
+ "lint-ci": "eslint . --max-warnings=0 --ext .js --ext .ts",
74
75
  "prepare": "husky install && yarn build",
75
76
  "prepack": "yarn build && yarn test && pinst --disable",
76
77
  "postpack": "pinst --enable"
77
78
  },
78
79
  "dependencies": {
79
- "@melloware/coloris": "^0.16.0",
80
+ "@melloware/coloris": "^0.16.1",
80
81
  "bezier-js": "^6.1.0"
81
82
  },
82
83
  "devDependencies": {
83
84
  "@types/bezier-js": "^4.1.0",
84
- "@types/jest": "^28.1.7",
85
+ "@types/jest": "^29.0.3",
85
86
  "@types/jsdom": "^20.0.0",
86
- "@types/node": "^18.7.15",
87
- "@typescript-eslint/eslint-plugin": "^5.36.2",
88
- "@typescript-eslint/parser": "^5.36.2",
87
+ "@types/node": "^18.7.23",
88
+ "@typescript-eslint/eslint-plugin": "^5.38.1",
89
+ "@typescript-eslint/parser": "^5.38.1",
89
90
  "css-loader": "^6.7.1",
90
- "eslint": "^8.23.0",
91
+ "eslint": "^8.24.0",
91
92
  "husky": "^8.0.1",
92
- "jest": "^28.1.3",
93
- "jest-environment-jsdom": "^29.0.2",
93
+ "jest": "^29.0.3",
94
+ "jest-environment-jsdom": "^29.0.3",
94
95
  "jsdom": "^20.0.0",
95
96
  "lint-staged": "^13.0.3",
96
97
  "pinst": "^3.0.0",
97
98
  "style-loader": "^3.3.1",
98
- "terser-webpack-plugin": "^5.3.5",
99
- "ts-jest": "^28.0.8",
100
- "ts-loader": "^9.3.1",
99
+ "terser-webpack-plugin": "^5.3.6",
100
+ "ts-jest": "^29.0.2",
101
+ "ts-loader": "^9.4.1",
101
102
  "ts-node": "^10.9.1",
102
- "typedoc": "^0.23.14",
103
- "typescript": "^4.8.2",
103
+ "typedoc": "^0.23.15",
104
+ "typescript": "^4.8.3",
104
105
  "webpack": "^5.74.0"
105
106
  },
106
107
  "bugs": {
package/src/Editor.ts CHANGED
@@ -797,6 +797,8 @@ export class Editor {
797
797
  public sendPenEvent(
798
798
  eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,
799
799
  point: Point2,
800
+
801
+ // @deprecated
800
802
  allPointers?: Pointer[]
801
803
  ) {
802
804
  const mainPointer = Pointer.ofCanvasPoint(
@@ -206,8 +206,10 @@ export default abstract class AbstractComponent {
206
206
 
207
207
  public abstract description(localizationTable: ImageComponentLocalization): string;
208
208
 
209
+ // Component-specific implementation of {@link clone}.
209
210
  protected abstract createClone(): AbstractComponent;
210
211
 
212
+ // Returns a copy of this component.
211
213
  public clone() {
212
214
  const clone = this.createClone();
213
215
 
@@ -2,12 +2,9 @@ import Color4 from '../Color4';
2
2
  import Path from '../math/Path';
3
3
  import { Vec2 } from '../math/Vec2';
4
4
  import Stroke from './Stroke';
5
- import { loadExpectExtensions } from '../testing/loadExpectExtensions';
6
5
  import createEditor from '../testing/createEditor';
7
6
  import Mat33 from '../math/Mat33';
8
7
 
9
- loadExpectExtensions();
10
-
11
8
  describe('Stroke', () => {
12
9
  it('empty stroke should have an empty bounding box', () => {
13
10
  const stroke = new Stroke([{
@@ -18,7 +18,8 @@ export default class Stroke extends AbstractComponent {
18
18
  public constructor(parts: RenderablePathSpec[]) {
19
19
  super('stroke');
20
20
 
21
- this.parts = parts.map((section): StrokePart => {
21
+ this.parts = [];
22
+ for (const section of parts) {
22
23
  const path = Path.fromRenderable(section);
23
24
  const pathBBox = this.bboxForPart(path.bbox, section.style);
24
25
 
@@ -28,15 +29,15 @@ export default class Stroke extends AbstractComponent {
28
29
  this.contentBBox = this.contentBBox.union(pathBBox);
29
30
  }
30
31
 
31
- return {
32
+ this.parts.push({
32
33
  path,
33
34
 
34
35
  // To implement RenderablePathSpec
35
36
  startPoint: path.startPoint,
36
37
  style: section.style,
37
38
  commands: path.parts,
38
- };
39
- });
39
+ });
40
+ }
40
41
  this.contentBBox ??= Rect2.empty;
41
42
  }
42
43
 
@@ -104,9 +105,15 @@ export default class Stroke extends AbstractComponent {
104
105
  }
105
106
 
106
107
  public getPath() {
107
- return this.parts.reduce((accumulator: Path|null, current: StrokePart) => {
108
- return accumulator?.union(current.path) ?? current.path;
109
- }, null) ?? Path.empty;
108
+ let result: Path|null = null;
109
+ for (const part of this.parts) {
110
+ if (result) {
111
+ result = result.union(part.path);
112
+ } else {
113
+ result ??= part.path;
114
+ }
115
+ }
116
+ return result ?? Path.empty;
110
117
  }
111
118
 
112
119
  public description(localization: ImageComponentLocalization): string {
@@ -3,9 +3,6 @@ import Mat33 from '../math/Mat33';
3
3
  import Rect2 from '../math/Rect2';
4
4
  import AbstractComponent from './AbstractComponent';
5
5
  import Text, { TextStyle } from './Text';
6
- import { loadExpectExtensions } from '../testing/loadExpectExtensions';
7
-
8
- loadExpectExtensions();
9
6
 
10
7
  const estimateTextBounds = (text: string, style: TextStyle): Rect2 => {
11
8
  const widthEst = text.length * style.size;
@@ -135,7 +135,7 @@ export default class Text extends AbstractComponent {
135
135
  return new Text(this.textObjects, this.transform, this.style);
136
136
  }
137
137
 
138
- private getText() {
138
+ public getText() {
139
139
  const result: string[] = [];
140
140
 
141
141
  for (const textObject of this.textObjects) {
@@ -146,7 +146,7 @@ export default class Text extends AbstractComponent {
146
146
  }
147
147
  }
148
148
 
149
- return result.join(' ');
149
+ return result.join('\n');
150
150
  }
151
151
 
152
152
  public description(localizationTable: ImageComponentLocalization): string {