js-draw 0.3.2 → 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.
- package/.github/pull_request_template.md +15 -0
- package/.github/workflows/firebase-hosting-merge.yml +7 -0
- package/.github/workflows/firebase-hosting-pull-request.yml +10 -0
- package/.github/workflows/github-pages.yml +2 -0
- package/CHANGELOG.md +16 -1
- package/README.md +1 -3
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +11 -0
- package/dist/src/Editor.js +107 -77
- package/dist/src/Pointer.d.ts +1 -1
- package/dist/src/Pointer.js +8 -3
- package/dist/src/Viewport.d.ts +1 -0
- package/dist/src/Viewport.js +14 -1
- package/dist/src/components/AbstractComponent.js +1 -0
- package/dist/src/components/ImageComponent.d.ts +2 -2
- package/dist/src/components/Stroke.js +15 -9
- package/dist/src/components/Text.d.ts +1 -1
- package/dist/src/components/Text.js +1 -1
- package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -0
- package/dist/src/components/builders/FreehandLineBuilder.js +34 -36
- package/dist/src/language/assertions.d.ts +1 -0
- package/dist/src/language/assertions.js +5 -0
- package/dist/src/math/Mat33.d.ts +38 -2
- package/dist/src/math/Mat33.js +30 -1
- package/dist/src/math/Path.d.ts +1 -1
- package/dist/src/math/Path.js +10 -8
- package/dist/src/math/Vec3.d.ts +12 -2
- package/dist/src/math/Vec3.js +16 -1
- package/dist/src/math/rounding.d.ts +1 -0
- package/dist/src/math/rounding.js +13 -6
- package/dist/src/rendering/renderers/AbstractRenderer.js +2 -1
- package/dist/src/testing/beforeEachFile.d.ts +1 -0
- package/dist/src/testing/beforeEachFile.js +3 -0
- package/dist/src/testing/createEditor.d.ts +1 -0
- package/dist/src/testing/createEditor.js +7 -1
- package/dist/src/testing/loadExpectExtensions.d.ts +0 -15
- package/dist/src/toolbar/HTMLToolbar.js +5 -4
- package/dist/src/toolbar/widgets/SelectionToolWidget.d.ts +1 -1
- package/dist/src/tools/PasteHandler.js +3 -1
- package/dist/src/tools/Pen.js +1 -1
- package/dist/src/tools/SelectionTool/Selection.d.ts +54 -0
- package/dist/src/tools/SelectionTool/Selection.js +337 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.d.ts +35 -0
- package/dist/src/tools/SelectionTool/SelectionHandle.js +75 -0
- package/dist/src/tools/SelectionTool/SelectionTool.d.ts +31 -0
- package/dist/src/tools/SelectionTool/SelectionTool.js +284 -0
- package/dist/src/tools/SelectionTool/TransformMode.d.ts +34 -0
- package/dist/src/tools/SelectionTool/TransformMode.js +98 -0
- package/dist/src/tools/SelectionTool/types.d.ts +9 -0
- package/dist/src/tools/SelectionTool/types.js +11 -0
- package/dist/src/tools/ToolController.js +1 -1
- package/dist/src/tools/lib.d.ts +1 -1
- package/dist/src/tools/lib.js +1 -1
- package/dist/src/types.d.ts +1 -1
- package/jest.config.js +5 -0
- package/package.json +15 -14
- package/src/Editor.css +1 -0
- package/src/Editor.ts +147 -108
- package/src/Pointer.ts +8 -3
- package/src/Viewport.ts +17 -2
- package/src/components/AbstractComponent.ts +4 -6
- package/src/components/ImageComponent.ts +2 -6
- package/src/components/Stroke.test.ts +0 -3
- package/src/components/Stroke.ts +14 -7
- package/src/components/Text.test.ts +0 -3
- package/src/components/Text.ts +4 -8
- package/src/components/builders/FreehandLineBuilder.ts +37 -43
- package/src/language/assertions.ts +6 -0
- package/src/math/LineSegment2.test.ts +8 -10
- package/src/math/Mat33.test.ts +14 -2
- package/src/math/Mat33.ts +43 -2
- package/src/math/Path.toString.test.ts +12 -1
- package/src/math/Path.ts +11 -9
- package/src/math/Rect2.test.ts +0 -3
- package/src/math/Vec2.test.ts +0 -3
- package/src/math/Vec3.test.ts +0 -3
- package/src/math/Vec3.ts +23 -2
- package/src/math/rounding.test.ts +30 -5
- package/src/math/rounding.ts +16 -7
- package/src/rendering/renderers/AbstractRenderer.ts +3 -2
- package/src/testing/beforeEachFile.ts +3 -0
- package/src/testing/createEditor.ts +8 -1
- package/src/testing/global.d.ts +17 -0
- package/src/testing/loadExpectExtensions.ts +0 -15
- package/src/toolbar/HTMLToolbar.ts +5 -4
- package/src/toolbar/toolbar.css +3 -2
- package/src/toolbar/widgets/SelectionToolWidget.ts +1 -1
- package/src/tools/PasteHandler.ts +4 -1
- package/src/tools/Pen.test.ts +150 -0
- package/src/tools/Pen.ts +1 -1
- package/src/tools/SelectionTool/Selection.ts +455 -0
- package/src/tools/SelectionTool/SelectionHandle.ts +99 -0
- package/src/tools/SelectionTool/SelectionTool.css +22 -0
- package/src/tools/{SelectionTool.test.ts → SelectionTool/SelectionTool.test.ts} +21 -21
- package/src/tools/SelectionTool/SelectionTool.ts +344 -0
- package/src/tools/SelectionTool/TransformMode.ts +114 -0
- package/src/tools/SelectionTool/types.ts +11 -0
- package/src/tools/ToolController.ts +1 -1
- package/src/tools/lib.ts +1 -1
- package/src/types.ts +1 -1
- package/tsconfig.json +3 -1
- package/dist/src/tools/SelectionTool.d.ts +0 -65
- package/dist/src/tools/SelectionTool.js +0 -647
- package/src/tools/SelectionTool.ts +0 -797
@@ -173,7 +173,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
173
173
|
}
|
174
174
|
|
175
175
|
public build(): Stroke {
|
176
|
-
if (this.lastPoint) {
|
176
|
+
if (this.lastPoint && (this.lowerSegments.length === 0 || this.approxCurrentCurveLength() > this.curveStartWidth * 2)) {
|
177
177
|
this.finalizeCurrentCurve();
|
178
178
|
}
|
179
179
|
return this.previewStroke()!;
|
@@ -189,6 +189,19 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
189
189
|
return Viewport.roundPoint(point, minFit);
|
190
190
|
}
|
191
191
|
|
192
|
+
// Returns the distance between the start, control, and end points of the curve.
|
193
|
+
private approxCurrentCurveLength() {
|
194
|
+
if (!this.currentCurve) {
|
195
|
+
return 0;
|
196
|
+
}
|
197
|
+
const startPt = Vec2.ofXY(this.currentCurve.points[0]);
|
198
|
+
const controlPt = Vec2.ofXY(this.currentCurve.points[1]);
|
199
|
+
const endPt = Vec2.ofXY(this.currentCurve.points[2]);
|
200
|
+
const toControlDist = startPt.minus(controlPt).length();
|
201
|
+
const toEndDist = endPt.minus(controlPt).length();
|
202
|
+
return toControlDist + toEndDist;
|
203
|
+
}
|
204
|
+
|
192
205
|
private finalizeCurrentCurve() {
|
193
206
|
// Case where no points have been added
|
194
207
|
if (!this.currentCurve) {
|
@@ -285,10 +298,8 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
285
298
|
startVec = startVec.times(this.curveStartWidth / 2);
|
286
299
|
endVec = endVec.times(this.curveEndWidth / 2);
|
287
300
|
|
288
|
-
if (
|
289
|
-
|
290
|
-
// fix.
|
291
|
-
console.error('startVec is NaN', startVec, endVec, this.currentCurve);
|
301
|
+
if (!isFinite(startVec.magnitude())) {
|
302
|
+
console.error('Warning: startVec is NaN or ∞', startVec, endVec, this.currentCurve);
|
292
303
|
startVec = endVec;
|
293
304
|
}
|
294
305
|
|
@@ -307,39 +318,22 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
307
318
|
}
|
308
319
|
|
309
320
|
const halfVecT = projectionT;
|
310
|
-
|
321
|
+
const halfVec = Vec2.ofXY(this.currentCurve.normal(halfVecT))
|
311
322
|
.normalized().times(
|
312
323
|
this.curveStartWidth / 2 * halfVecT
|
313
324
|
+ this.curveEndWidth / 2 * (1 - halfVecT)
|
314
325
|
);
|
315
326
|
|
316
|
-
// Computes a boundary curve. [direction] should be either +1 or -1 (determines the side
|
317
|
-
// of the center curve to place the boundary).
|
318
|
-
const computeBoundaryCurve = (direction: number, halfVec: Vec2) => {
|
319
|
-
return new Bezier(
|
320
|
-
startPt.plus(startVec.times(direction)),
|
321
|
-
controlPoint.plus(halfVec.times(direction)),
|
322
|
-
endPt.plus(endVec.times(direction)),
|
323
|
-
);
|
324
|
-
};
|
325
|
-
|
326
|
-
const boundariesIntersect = () => {
|
327
|
-
const upperBoundary = computeBoundaryCurve(1, halfVec);
|
328
|
-
const lowerBoundary = computeBoundaryCurve(-1, halfVec);
|
329
|
-
return upperBoundary.intersects(lowerBoundary).length > 0;
|
330
|
-
};
|
331
|
-
|
332
|
-
// If the boundaries have intersections, increasing the half vector's length could fix this.
|
333
|
-
if (boundariesIntersect()) {
|
334
|
-
halfVec = halfVec.times(1.1);
|
335
|
-
}
|
336
|
-
|
337
327
|
// Each starts at startPt ± startVec
|
328
|
+
const lowerCurveControlPoint = this.roundPoint(controlPoint.plus(halfVec));
|
329
|
+
const lowerCurveEndPoint = this.roundPoint(endPt.plus(endVec));
|
330
|
+
const upperCurveControlPoint = this.roundPoint(controlPoint.minus(halfVec));
|
331
|
+
const upperCurveStartPoint = this.roundPoint(endPt.minus(endVec));
|
338
332
|
|
339
333
|
const lowerCurve: QuadraticBezierPathCommand = {
|
340
334
|
kind: PathCommandType.QuadraticBezierTo,
|
341
|
-
controlPoint:
|
342
|
-
endPoint:
|
335
|
+
controlPoint: lowerCurveControlPoint,
|
336
|
+
endPoint: lowerCurveEndPoint,
|
343
337
|
};
|
344
338
|
|
345
339
|
// From the end of the upperCurve to the start of the lowerCurve:
|
@@ -351,12 +345,12 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
351
345
|
// From the end of lowerCurve to the start of upperCurve:
|
352
346
|
const lowerToUpperConnector: LinePathCommand = {
|
353
347
|
kind: PathCommandType.LineTo,
|
354
|
-
point:
|
348
|
+
point: upperCurveStartPoint,
|
355
349
|
};
|
356
350
|
|
357
351
|
const upperCurve: QuadraticBezierPathCommand = {
|
358
352
|
kind: PathCommandType.QuadraticBezierTo,
|
359
|
-
controlPoint:
|
353
|
+
controlPoint: upperCurveControlPoint,
|
360
354
|
endPoint: this.roundPoint(startPt.minus(startVec)),
|
361
355
|
};
|
362
356
|
|
@@ -374,7 +368,6 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
374
368
|
const fuzzEq = 1e-10;
|
375
369
|
const deltaTime = newPoint.time - this.lastPoint.time;
|
376
370
|
if (newPoint.pos.eq(this.lastPoint.pos, fuzzEq) || deltaTime === 0) {
|
377
|
-
console.warn('Discarding identical point');
|
378
371
|
return;
|
379
372
|
} else if (isNaN(newPoint.pos.magnitude())) {
|
380
373
|
console.warn('Discarding NaN point.', newPoint);
|
@@ -433,23 +426,23 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
433
426
|
let exitingVec = this.computeExitingVec();
|
434
427
|
|
435
428
|
// Find the intersection between the entering vector and the exiting vector
|
436
|
-
const maxRelativeLength =
|
429
|
+
const maxRelativeLength = 3;
|
437
430
|
const segmentStart = this.buffer[0];
|
438
431
|
const segmentEnd = newPoint.pos;
|
439
432
|
const startEndDist = segmentEnd.minus(segmentStart).magnitude();
|
440
433
|
const maxControlPointDist = maxRelativeLength * startEndDist;
|
441
434
|
|
442
435
|
// Exit in cases where we would divide by zero
|
443
|
-
if (maxControlPointDist === 0 || exitingVec.magnitude() === 0 ||
|
436
|
+
if (maxControlPointDist === 0 || exitingVec.magnitude() === 0 || !isFinite(exitingVec.magnitude())) {
|
444
437
|
return;
|
445
438
|
}
|
446
439
|
|
447
|
-
console.assert(
|
440
|
+
console.assert(isFinite(enteringVec.magnitude()), 'Pre-normalized enteringVec has NaN or ∞ magnitude!');
|
448
441
|
|
449
442
|
enteringVec = enteringVec.normalized();
|
450
443
|
exitingVec = exitingVec.normalized();
|
451
444
|
|
452
|
-
console.assert(
|
445
|
+
console.assert(isFinite(enteringVec.magnitude()), 'Normalized enteringVec has NaN or ∞ magnitude!');
|
453
446
|
|
454
447
|
const lineFromStart = new LineSegment2(
|
455
448
|
segmentStart,
|
@@ -462,18 +455,20 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
462
455
|
const intersection = lineFromEnd.intersection(lineFromStart);
|
463
456
|
|
464
457
|
// Position the control point at this intersection
|
465
|
-
let controlPoint: Point2;
|
458
|
+
let controlPoint: Point2|null = null;
|
466
459
|
if (intersection) {
|
467
460
|
controlPoint = intersection.point;
|
468
|
-
}
|
461
|
+
}
|
462
|
+
|
463
|
+
// No intersection or the intersection is one of the end points?
|
464
|
+
if (!controlPoint || segmentStart.eq(controlPoint) || segmentEnd.eq(controlPoint)) {
|
469
465
|
// Position the control point closer to the first -- the connecting
|
470
466
|
// segment will be roughly a line.
|
471
467
|
controlPoint = segmentStart.plus(enteringVec.times(startEndDist / 4));
|
472
468
|
}
|
473
469
|
|
474
|
-
|
475
|
-
|
476
|
-
}
|
470
|
+
console.assert(!segmentStart.eq(controlPoint, 1e-11), 'Start and control points are equal!');
|
471
|
+
console.assert(!controlPoint.eq(segmentEnd, 1e-11), 'Control and end points are equal!');
|
477
472
|
|
478
473
|
const prevCurve = this.currentCurve;
|
479
474
|
this.currentCurve = new Bezier(segmentStart.xy, controlPoint.xy, segmentEnd.xy);
|
@@ -502,8 +497,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
502
497
|
return true;
|
503
498
|
};
|
504
499
|
|
505
|
-
|
506
|
-
if (this.buffer.length > 3 && approxCurveLen > this.curveEndWidth / 3) {
|
500
|
+
if (this.buffer.length > 3 && this.approxCurrentCurveLength() > this.curveStartWidth) {
|
507
501
|
if (!curveMatchesPoints(this.currentCurve)) {
|
508
502
|
// Use a curve that better fits the points
|
509
503
|
this.currentCurve = prevCurve;
|
@@ -1,9 +1,7 @@
|
|
1
1
|
import LineSegment2 from './LineSegment2';
|
2
|
-
import { loadExpectExtensions } from '../testing/loadExpectExtensions';
|
3
2
|
import { Vec2 } from './Vec2';
|
4
3
|
import Mat33 from './Mat33';
|
5
4
|
|
6
|
-
loadExpectExtensions();
|
7
5
|
|
8
6
|
describe('Line2', () => {
|
9
7
|
it('x and y axes should intersect at (0, 0)', () => {
|
@@ -14,13 +12,13 @@ describe('Line2', () => {
|
|
14
12
|
});
|
15
13
|
|
16
14
|
it('y = -2x + 2 and y = 2x - 2 should intersect at (1,0)', () => {
|
17
|
-
// y = -
|
15
|
+
// y = -4x + 2
|
18
16
|
const line1 = new LineSegment2(Vec2.of(0, 2), Vec2.of(1, -2));
|
19
|
-
// y =
|
17
|
+
// y = 4x - 2
|
20
18
|
const line2 = new LineSegment2(Vec2.of(0, -2), Vec2.of(1, 2));
|
21
19
|
|
22
|
-
expect(line1.intersection(line2)?.point).objEq(Vec2.of(
|
23
|
-
expect(line2.intersection(line1)?.point).objEq(Vec2.of(
|
20
|
+
expect(line1.intersection(line2)?.point).objEq(Vec2.of(0.5, 0));
|
21
|
+
expect(line2.intersection(line1)?.point).objEq(Vec2.of(0.5, 0));
|
24
22
|
});
|
25
23
|
|
26
24
|
it('line from (10, 10) to (-100, 10) should intersect with the y-axis at t = 10', () => {
|
@@ -81,14 +79,14 @@ describe('Line2', () => {
|
|
81
79
|
expect(line.closestPointTo(Vec2.zero)).objEq(Vec2.of(1, 0));
|
82
80
|
});
|
83
81
|
|
84
|
-
it('Closest point from (-1
|
82
|
+
it('Closest point from (-1,-2) to segment((1,1) -> (2,4)) should be (1,1)', () => {
|
85
83
|
const line = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 4));
|
86
|
-
expect(line.closestPointTo(Vec2.of(-1, 2))).objEq(Vec2.of(1, 1));
|
84
|
+
expect(line.closestPointTo(Vec2.of(-1, -2))).objEq(Vec2.of(1, 1));
|
87
85
|
});
|
88
86
|
|
89
|
-
it('Closest point from (5,
|
87
|
+
it('Closest point from (5,8) to segment((1,1) -> (2,4)) should be (2,4)', () => {
|
90
88
|
const line = new LineSegment2(Vec2.of(1, 1), Vec2.of(2, 4));
|
91
|
-
expect(line.closestPointTo(Vec2.of(5,
|
89
|
+
expect(line.closestPointTo(Vec2.of(5, 8))).objEq(Vec2.of(2, 4));
|
92
90
|
});
|
93
91
|
|
94
92
|
it('Should translate when translated by a translation matrix', () => {
|
package/src/math/Mat33.test.ts
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
import Mat33 from './Mat33';
|
2
2
|
import { Vec2 } from './Vec2';
|
3
|
-
import { loadExpectExtensions } from '../testing/loadExpectExtensions';
|
4
3
|
import Vec3 from './Vec3';
|
5
4
|
|
6
|
-
loadExpectExtensions();
|
7
5
|
|
8
6
|
describe('Mat33 tests', () => {
|
9
7
|
it('equality', () => {
|
@@ -142,6 +140,20 @@ describe('Mat33 tests', () => {
|
|
142
140
|
).objEq(Vec2.unitX, fuzz);
|
143
141
|
});
|
144
142
|
|
143
|
+
it('should correctly apply a mapping to all components', () => {
|
144
|
+
expect(
|
145
|
+
new Mat33(
|
146
|
+
1, 2, 3,
|
147
|
+
4, 5, 6,
|
148
|
+
7, 8, 9,
|
149
|
+
).mapEntries(component => component - 1)
|
150
|
+
).toMatchObject(new Mat33(
|
151
|
+
0, 1, 2,
|
152
|
+
3, 4, 5,
|
153
|
+
6, 7, 8,
|
154
|
+
));
|
155
|
+
});
|
156
|
+
|
145
157
|
it('should convert CSS matrix(...) strings to matricies', () => {
|
146
158
|
// From MDN:
|
147
159
|
// ⎡ a c e ⎤
|
package/src/math/Mat33.ts
CHANGED
@@ -1,6 +1,12 @@
|
|
1
1
|
import { Point2, Vec2 } from './Vec2';
|
2
2
|
import Vec3 from './Vec3';
|
3
3
|
|
4
|
+
export type Mat33Array = [
|
5
|
+
number, number, number,
|
6
|
+
number, number, number,
|
7
|
+
number, number, number,
|
8
|
+
];
|
9
|
+
|
4
10
|
/**
|
5
11
|
* Represents a three dimensional linear transformation or
|
6
12
|
* a two-dimensional affine transformation. (An affine transformation scales/rotates/shears
|
@@ -239,7 +245,7 @@ export default class Mat33 {
|
|
239
245
|
* ...
|
240
246
|
* ```
|
241
247
|
*/
|
242
|
-
public toArray():
|
248
|
+
public toArray(): Mat33Array {
|
243
249
|
return [
|
244
250
|
this.a1, this.a2, this.a3,
|
245
251
|
this.b1, this.b2, this.b3,
|
@@ -247,6 +253,27 @@ export default class Mat33 {
|
|
247
253
|
];
|
248
254
|
}
|
249
255
|
|
256
|
+
/**
|
257
|
+
* @example
|
258
|
+
* ```
|
259
|
+
* new Mat33(
|
260
|
+
* 1, 2, 3,
|
261
|
+
* 4, 5, 6,
|
262
|
+
* 7, 8, 9,
|
263
|
+
* ).mapEntries(component => component - 1);
|
264
|
+
* // → ⎡ 0, 1, 2 ⎤
|
265
|
+
* // ⎢ 3, 4, 5 ⎥
|
266
|
+
* // ⎣ 6, 7, 8 ⎦
|
267
|
+
* ```
|
268
|
+
*/
|
269
|
+
public mapEntries(mapping: (component: number)=>number): Mat33 {
|
270
|
+
return new Mat33(
|
271
|
+
mapping(this.a1), mapping(this.a2), mapping(this.a3),
|
272
|
+
mapping(this.b1), mapping(this.b2), mapping(this.b3),
|
273
|
+
mapping(this.c1), mapping(this.c2), mapping(this.c3),
|
274
|
+
);
|
275
|
+
}
|
276
|
+
|
250
277
|
/** Constructs a 3x3 translation matrix (for translating `Vec2`s) */
|
251
278
|
public static translation(amount: Vec2): Mat33 {
|
252
279
|
// When transforming Vec2s by a 3x3 matrix, we give the input
|
@@ -297,7 +324,21 @@ export default class Mat33 {
|
|
297
324
|
return result.rightMul(Mat33.translation(center.times(-1)));
|
298
325
|
}
|
299
326
|
|
300
|
-
/**
|
327
|
+
/** @see {@link !fromCSSMatrix} */
|
328
|
+
public toCSSMatrix(): string {
|
329
|
+
return `matrix(${this.a1},${this.b1},${this.a2},${this.b2},${this.a3},${this.b3})`;
|
330
|
+
}
|
331
|
+
|
332
|
+
/**
|
333
|
+
* Converts a CSS-form `matrix(a, b, c, d, e, f)` to a Mat33.
|
334
|
+
*
|
335
|
+
* Note that such a matrix has the form,
|
336
|
+
* ```
|
337
|
+
* ⎡ a c e ⎤
|
338
|
+
* ⎢ b d f ⎥
|
339
|
+
* ⎣ 0 0 1 ⎦
|
340
|
+
* ```
|
341
|
+
*/
|
301
342
|
public static fromCSSMatrix(cssString: string): Mat33 {
|
302
343
|
if (cssString === '' || cssString === 'none') {
|
303
344
|
return Mat33.identity;
|
@@ -38,7 +38,7 @@ describe('Path.toString', () => {
|
|
38
38
|
const path = new Path(Vec2.of(1000, 2_000_000), [
|
39
39
|
{
|
40
40
|
kind: PathCommandType.LineTo,
|
41
|
-
point: Vec2.of(30.
|
41
|
+
point: Vec2.of(30.00000001, 40.000000001),
|
42
42
|
},
|
43
43
|
]);
|
44
44
|
|
@@ -53,4 +53,15 @@ describe('Path.toString', () => {
|
|
53
53
|
'M100,100', 'l1,1', 'q1,1 -11-11', 'l10,10'
|
54
54
|
].join(''));
|
55
55
|
});
|
56
|
+
|
57
|
+
it('should not lose precision when saving', () => {
|
58
|
+
const pathStr = 'M184.2,52.3l-.2-.2q-2.7,2.4 -3.2,3.5q-2.8,7 -.9,6.1q4.3-2.6 4.8-6.1q1.2-8.8 .4-8.3q-4.2,5.2 -3.9,3.9q.2-1.6 .3-2.1q.2-1.3 -.2-1q-3.8,6.5 -3.2,3.3q.6-4.1 1.1-5.3q4.1-10 3.3-8.3q-5.3,13.1 -6.6,14.1q-3.3,2.8 -1.8-1.5q2.8-9.7 2.7-8.4q0,.3 0,.4q-1.4,7.1 -2.7,8.5q-2.6,3.2 -2.5,2.9q-.3-1.9 -.7-1.9q-4.1,4.4 -2.9,1.9q1.1-3 .3-2.6q-1.8,2 -2.5,2.4q-4.5,2.8 -4.2,1.9q.3-1.6 .2-1.4q1.5,2.2 1.3,2.9q-.8,3.9 -.5,3.3q.8-7.6 2.5-13.3q2.6-9.2 2.9-6.9q.3,1.4 .3,1.2q-.7-.4 -.9,0q-2.2,11.6 -7.6,13.6q-3.9,1.6 -2.1-1.3q3-5.5 2.6-3.4q-.2,1.8 -.5,1.8q-3.2,.5 -4.1,1.2q-2.6,2.6 -1.9,2.5q4.7-4.4 3.7-5.5q-1.1-.9 -1.6-.6q-7.2,7.5 -3.9,6.5q.3-.1 .4-.4q.6-5.3 -.2-4.9q-2.8,2.3 -3.1,2.4q-3.7,1.5 -3.5,.5q.3-3.6 1.4-3.3q3.5,.7 1.9,2.4q-1.7,2.3 -1.6,.8q0-3.5 -.9-3.1q-5.1,3.3 -4.9,2.8q.1-4 -.8-3.5q-4.3,3.4 -4.6,2.5q-1-2.1 .5-8.7l-.2,0q-1.6,6.6 -.7,8.9q.7,1.2 5.2-2.3q.4-.5 .2,3.1q.1,1 5.5-2.4q.4-.4 .3,2.7q.1,2 2.4-.4q1.7-2.3 -2.1-3.2q-1.7-.3 -2,3.7q0,1.4 4.1-.1q.3-.1 3.1-2.4q.3-.5 -.4,4.5q0-.1 -.2,0q-2.6,1.2 4.5-5.7q0-.2 .8,.6q.9,.6 -3.7,4.7q-.5,1 2.7-1.7q.6-.7 3.7-1.2q.7-.2 .9-2.2q.1-2.7 -3.4,3.2q-1.8,3.4 2.7,1.9q5.6-2.1 7.8-14q-.1,.1 .3,.4q.6,.1 .3-1.6q-.7-2.8 -3.7,6.7q-1.8,5.8 -2.5,13.5q.1,1.1 1.3-3.1q.2-1 -1.3-3.3q-.5-.5 -1,1.6q-.1,1.3 4.8-1.5q1-1 3-2q.1-.4 -1.1,2q-1.1,3.1 3.7-1.3q-.4,0 -.1,1.5q.3,.8 3.3-2.5q1.3-1.6 2.7-8.9q0-.1 0-.4q-.3-1.9 -3.5,8.2q-1.3,4.9 2.4,2.1q1.4-1.2 6.6-14.3q.8-2.4 -3.9,7.9q-.6,1.3 -1.1,5.5q-.3,3.7 4-3.1q-.2,0 -.6,.6q-.2,.6 -.3,2.3q0,1.8 4.7-3.5q.1-.5 -1.2,7.9q-.5,3.2 -4.6,5.7q-1.3,1 1.5-5.5q.4-1.1 3.01-3.5';
|
59
|
+
|
60
|
+
const path1 = Path.fromString(pathStr);
|
61
|
+
path1['cachedStringVersion'] = null; // Clear the cache.
|
62
|
+
const path = Path.fromString(path1.toString(true));
|
63
|
+
path1['cachedStringVersion'] = null; // Clear the cache.
|
64
|
+
|
65
|
+
expect(path.toString(true)).toBe(path1.toString(true));
|
66
|
+
});
|
56
67
|
});
|
package/src/math/Path.ts
CHANGED
@@ -378,15 +378,17 @@ export default class Path {
|
|
378
378
|
|
379
379
|
private cachedStringVersion: string|null = null;
|
380
380
|
|
381
|
-
public toString(): string {
|
381
|
+
public toString(useNonAbsCommands?: boolean): string {
|
382
382
|
if (this.cachedStringVersion) {
|
383
383
|
return this.cachedStringVersion;
|
384
384
|
}
|
385
385
|
|
386
|
-
|
387
|
-
|
386
|
+
if (useNonAbsCommands === undefined) {
|
387
|
+
// Hueristic: Try to determine whether converting absolute to relative commands is worth it.
|
388
|
+
useNonAbsCommands = Math.abs(this.bbox.topLeft.x) > 10 && Math.abs(this.bbox.topLeft.y) > 10;
|
389
|
+
}
|
388
390
|
|
389
|
-
const result = Path.toString(this.startPoint, this.parts, !
|
391
|
+
const result = Path.toString(this.startPoint, this.parts, !useNonAbsCommands);
|
390
392
|
this.cachedStringVersion = result;
|
391
393
|
return result;
|
392
394
|
}
|
@@ -409,10 +411,13 @@ export default class Path {
|
|
409
411
|
const roundedPrevY = prevPoint ? toRoundedString(prevPoint.y) : '';
|
410
412
|
|
411
413
|
for (const point of points) {
|
414
|
+
const xComponent = toRoundedString(point.x);
|
415
|
+
const yComponent = toRoundedString(point.y);
|
416
|
+
|
412
417
|
// Relative commands are often shorter as strings than absolute commands.
|
413
418
|
if (!makeAbsCommand) {
|
414
|
-
const xComponentRelative = toStringOfSamePrecision(point.x - prevPoint!.x, roundedPrevX, roundedPrevY);
|
415
|
-
const yComponentRelative = toStringOfSamePrecision(point.y - prevPoint!.y, roundedPrevX, roundedPrevY);
|
419
|
+
const xComponentRelative = toStringOfSamePrecision(point.x - prevPoint!.x, xComponent, roundedPrevX, roundedPrevY);
|
420
|
+
const yComponentRelative = toStringOfSamePrecision(point.y - prevPoint!.y, yComponent, roundedPrevX, roundedPrevY);
|
416
421
|
|
417
422
|
// No need for an additional separator if it starts with a '-'
|
418
423
|
if (yComponentRelative.charAt(0) === '-') {
|
@@ -421,9 +426,6 @@ export default class Path {
|
|
421
426
|
relativeCommandParts.push(`${xComponentRelative},${yComponentRelative}`);
|
422
427
|
}
|
423
428
|
} else {
|
424
|
-
const xComponent = toRoundedString(point.x);
|
425
|
-
const yComponent = toRoundedString(point.y);
|
426
|
-
|
427
429
|
absoluteCommandParts.push(`${xComponent},${yComponent}`);
|
428
430
|
}
|
429
431
|
}
|
package/src/math/Rect2.test.ts
CHANGED
@@ -1,11 +1,8 @@
|
|
1
1
|
|
2
2
|
import Rect2 from './Rect2';
|
3
3
|
import { Vec2 } from './Vec2';
|
4
|
-
import loadExpectExtensions from '../testing/loadExpectExtensions';
|
5
4
|
import Mat33 from './Mat33';
|
6
5
|
|
7
|
-
loadExpectExtensions();
|
8
|
-
|
9
6
|
describe('Rect2', () => {
|
10
7
|
it('width, height should always be positive', () => {
|
11
8
|
expect(new Rect2(-1, -2, -3, 4)).objEq(new Rect2(-4, -2, 3, 4));
|
package/src/math/Vec2.test.ts
CHANGED
package/src/math/Vec3.test.ts
CHANGED
package/src/math/Vec3.ts
CHANGED
@@ -95,6 +95,27 @@ export default class Vec3 {
|
|
95
95
|
);
|
96
96
|
}
|
97
97
|
|
98
|
+
/**
|
99
|
+
* If `other` is a `Vec3`, multiplies `this` component-wise by `other`. Otherwise,
|
100
|
+
* if `other is a `number`, returns the result of scalar multiplication.
|
101
|
+
*
|
102
|
+
* @example
|
103
|
+
* ```
|
104
|
+
* Vec3.of(1, 2, 3).scale(Vec3.of(2, 4, 6)); // → Vec3(2, 8, 18)
|
105
|
+
* ```
|
106
|
+
*/
|
107
|
+
public scale(other: Vec3|number): Vec3 {
|
108
|
+
if (typeof other === 'number') {
|
109
|
+
return this.times(other);
|
110
|
+
}
|
111
|
+
|
112
|
+
return Vec3.of(
|
113
|
+
this.x * other.x,
|
114
|
+
this.y * other.y,
|
115
|
+
this.z * other.z,
|
116
|
+
);
|
117
|
+
}
|
118
|
+
|
98
119
|
/**
|
99
120
|
* Returns a vector orthogonal to this. If this is a Vec2, returns `this` rotated
|
100
121
|
* 90 degrees counter-clockwise.
|
@@ -158,7 +179,7 @@ export default class Vec3 {
|
|
158
179
|
);
|
159
180
|
}
|
160
181
|
|
161
|
-
public asArray(): number
|
182
|
+
public asArray(): [ number, number, number ] {
|
162
183
|
return [this.x, this.y, this.z];
|
163
184
|
}
|
164
185
|
|
@@ -175,7 +196,7 @@ export default class Vec3 {
|
|
175
196
|
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 2.99); // → false
|
176
197
|
* ```
|
177
198
|
*/
|
178
|
-
public eq(other: Vec3, fuzz: number): boolean {
|
199
|
+
public eq(other: Vec3, fuzz: number = 1e-10): boolean {
|
179
200
|
for (let i = 0; i < 3; i++) {
|
180
201
|
if (Math.abs(other.at(i) - this.at(i)) > fuzz) {
|
181
202
|
return false;
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { toRoundedString, toStringOfSamePrecision } from './rounding';
|
1
|
+
import { cleanUpNumber, toRoundedString, toStringOfSamePrecision } from './rounding';
|
2
2
|
|
3
3
|
describe('toRoundedString', () => {
|
4
4
|
it('should round up numbers endings similar to .999999999999999', () => {
|
@@ -12,18 +12,28 @@ describe('toRoundedString', () => {
|
|
12
12
|
expect(toRoundedString(10.999999998)).toBe('11');
|
13
13
|
});
|
14
14
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
15
|
+
it('should round strings with multiple digits after the ending decimal points', () => {
|
16
|
+
expect(toRoundedString(292.2 - 292.8)).toBe('-.6');
|
17
|
+
expect(toRoundedString(4.06425600000023)).toBe('4.064256');
|
18
|
+
});
|
19
19
|
|
20
20
|
it('should round down strings ending endings similar to .00000001', () => {
|
21
21
|
expect(toRoundedString(10.00000001)).toBe('10');
|
22
|
+
expect(toRoundedString(-30.00000001)).toBe('-30');
|
23
|
+
expect(toRoundedString(-14.20000000000002)).toBe('-14.2');
|
24
|
+
});
|
25
|
+
|
26
|
+
it('should not round numbers insufficiently close to the next', () => {
|
27
|
+
expect(toRoundedString(-10.9999)).toBe('-10.9999');
|
28
|
+
expect(toRoundedString(-10.0001)).toBe('-10.0001');
|
29
|
+
expect(toRoundedString(-10.123499)).toBe('-10.123499');
|
30
|
+
expect(toRoundedString(0.00123499)).toBe('.00123499');
|
22
31
|
});
|
23
32
|
});
|
24
33
|
|
25
34
|
it('toStringOfSamePrecision', () => {
|
26
35
|
expect(toStringOfSamePrecision(1.23456, '1.12')).toBe('1.23');
|
36
|
+
expect(toStringOfSamePrecision(1.23456, '1.120')).toBe('1.235');
|
27
37
|
expect(toStringOfSamePrecision(1.23456, '1.1')).toBe('1.2');
|
28
38
|
expect(toStringOfSamePrecision(1.23456, '1.1', '5.32')).toBe('1.23');
|
29
39
|
expect(toStringOfSamePrecision(-1.23456, '1.1', '5.32')).toBe('-1.23');
|
@@ -37,4 +47,19 @@ it('toStringOfSamePrecision', () => {
|
|
37
47
|
expect(toStringOfSamePrecision(-0.09999999999999432, '291.3')).toBe('-.1');
|
38
48
|
expect(toStringOfSamePrecision(-0.9999999999999432, '291.3')).toBe('-1');
|
39
49
|
expect(toStringOfSamePrecision(9998.9, '.1', '-11')).toBe('9998.9');
|
50
|
+
expect(toStringOfSamePrecision(-14.20000000000002, '.000001', '-11')).toBe('-14.2');
|
51
|
+
});
|
52
|
+
|
53
|
+
it('cleanUpNumber', () => {
|
54
|
+
expect(cleanUpNumber('000.0000')).toBe('0');
|
55
|
+
expect(cleanUpNumber('-000.0000')).toBe('0');
|
56
|
+
expect(cleanUpNumber('0.0000')).toBe('0');
|
57
|
+
expect(cleanUpNumber('0.001')).toBe('.001');
|
58
|
+
expect(cleanUpNumber('-0.001')).toBe('-.001');
|
59
|
+
expect(cleanUpNumber('-0.000000001')).toBe('-.000000001');
|
60
|
+
expect(cleanUpNumber('-0.00000000100')).toBe('-.000000001');
|
61
|
+
expect(cleanUpNumber('1234')).toBe('1234');
|
62
|
+
expect(cleanUpNumber('1234.5')).toBe('1234.5');
|
63
|
+
expect(cleanUpNumber('1234.500')).toBe('1234.5');
|
64
|
+
expect(cleanUpNumber('1.1368683772161603e-13')).toBe('0');
|
40
65
|
});
|
package/src/math/rounding.ts
CHANGED
@@ -1,9 +1,17 @@
|
|
1
1
|
// @packageDocumentation @internal
|
2
2
|
|
3
3
|
// Clean up stringified numbers
|
4
|
-
const cleanUpNumber = (text: string) => {
|
4
|
+
export const cleanUpNumber = (text: string) => {
|
5
5
|
// Regular expression substitions can be somewhat expensive. Only do them
|
6
6
|
// if necessary.
|
7
|
+
|
8
|
+
if (text.indexOf('e') > 0) {
|
9
|
+
// Round to zero.
|
10
|
+
if (text.match(/[eE][-]\d{2,}$/)) {
|
11
|
+
return '0';
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
7
15
|
const lastChar = text.charAt(text.length - 1);
|
8
16
|
if (lastChar === '0' || lastChar === '.') {
|
9
17
|
// Remove trailing zeroes
|
@@ -12,10 +20,6 @@ const cleanUpNumber = (text: string) => {
|
|
12
20
|
|
13
21
|
// Remove trailing period
|
14
22
|
text = text.replace(/[.]$/, '');
|
15
|
-
|
16
|
-
if (text === '-0') {
|
17
|
-
return '0';
|
18
|
-
}
|
19
23
|
}
|
20
24
|
|
21
25
|
const firstChar = text.charAt(0);
|
@@ -23,6 +27,11 @@ const cleanUpNumber = (text: string) => {
|
|
23
27
|
// Remove unnecessary leading zeroes.
|
24
28
|
text = text.replace(/^(0+)[.]/, '.');
|
25
29
|
text = text.replace(/^-(0+)[.]/, '-.');
|
30
|
+
text = text.replace(/^(-?)0+$/, '$10');
|
31
|
+
}
|
32
|
+
|
33
|
+
if (text === '-0') {
|
34
|
+
return '0';
|
26
35
|
}
|
27
36
|
|
28
37
|
return text;
|
@@ -31,8 +40,8 @@ const cleanUpNumber = (text: string) => {
|
|
31
40
|
export const toRoundedString = (num: number): string => {
|
32
41
|
// Try to remove rounding errors. If the number ends in at least three/four zeroes
|
33
42
|
// (or nines) just one or two digits, it's probably a rounding error.
|
34
|
-
const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,
|
35
|
-
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,
|
43
|
+
const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,4}$/;
|
44
|
+
const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,4}$/;
|
36
45
|
|
37
46
|
let text = num.toString(10);
|
38
47
|
if (text.indexOf('.') === -1) {
|
@@ -122,8 +122,9 @@ export default abstract class AbstractRenderer {
|
|
122
122
|
}
|
123
123
|
}
|
124
124
|
|
125
|
-
// Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill]
|
126
|
-
|
125
|
+
// Draw a rectangle. Boundary lines have width [lineWidth] and are filled with [lineFill].
|
126
|
+
// This is equivalent to `drawPath(Path.fromRect(...).toRenderable(...))`.
|
127
|
+
public drawRect(rect: Rect2, lineWidth: number, lineFill: RenderingStyle) {
|
127
128
|
const path = Path.fromRect(rect, lineWidth);
|
128
129
|
this.drawPath(path.toRenderable(lineFill));
|
129
130
|
}
|
@@ -1,4 +1,11 @@
|
|
1
1
|
import { RenderingMode } from '../rendering/Display';
|
2
2
|
import Editor from '../Editor';
|
3
3
|
|
4
|
-
|
4
|
+
/** Creates an editor. Should only be used in test files. */
|
5
|
+
export default () => {
|
6
|
+
if (jest === undefined) {
|
7
|
+
throw new Error('Files in the testing/ folder should only be used in tests!');
|
8
|
+
}
|
9
|
+
|
10
|
+
return new Editor(document.body, { renderingMode: RenderingMode.DummyRenderer });
|
11
|
+
};
|
@@ -0,0 +1,17 @@
|
|
1
|
+
|
2
|
+
|
3
|
+
// Type declarations for custom matchers
|
4
|
+
interface CustomMatchers<R = unknown> {
|
5
|
+
objEq(expected: {
|
6
|
+
eq: (other: any, ...args: any)=> boolean;
|
7
|
+
}, ...opts: any): R;
|
8
|
+
}
|
9
|
+
|
10
|
+
declare namespace jest {
|
11
|
+
interface Expect extends CustomMatchers {}
|
12
|
+
interface Matchers<R> extends CustomMatchers<R> {}
|
13
|
+
interface AsyncAsymmetricMatchers extends CustomMatchers {}
|
14
|
+
}
|
15
|
+
|
16
|
+
declare interface JestMatchers<T> extends CustomMatchers<T> {
|
17
|
+
}
|