js-draw 0.4.0 → 0.5.0
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 +15 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +1 -1
- package/dist/src/Editor.js +8 -3
- package/dist/src/components/AbstractComponent.js +1 -0
- 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 +33 -35
- package/dist/src/math/Vec3.d.ts +1 -1
- package/dist/src/math/Vec3.js +1 -1
- package/dist/src/rendering/renderers/SVGRenderer.js +1 -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/widgets/BaseWidget.d.ts +2 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +15 -0
- package/dist/src/toolbar/widgets/PenToolWidget.d.ts +2 -0
- package/dist/src/toolbar/widgets/PenToolWidget.js +14 -0
- package/dist/src/tools/SelectionTool/SelectionTool.js +8 -0
- package/dist/src/tools/ToolController.js +2 -0
- package/dist/src/tools/ToolbarShortcutHandler.d.ts +12 -0
- package/dist/src/tools/ToolbarShortcutHandler.js +23 -0
- package/dist/src/tools/lib.d.ts +1 -0
- package/dist/src/tools/lib.js +1 -0
- package/dist/src/types.d.ts +4 -2
- package/jest.config.js +5 -0
- package/package.json +15 -14
- package/src/Editor.ts +8 -2
- package/src/components/AbstractComponent.ts +2 -0
- 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 +2 -2
- package/src/components/builders/FreehandLineBuilder.ts +36 -42
- package/src/language/assertions.ts +2 -2
- package/src/math/LineSegment2.test.ts +8 -10
- package/src/math/Mat33.test.ts +0 -2
- 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 +1 -1
- package/src/rendering/renderers/SVGRenderer.ts +1 -1
- 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/toolbar.css +3 -2
- package/src/toolbar/widgets/BaseWidget.ts +19 -1
- package/src/toolbar/widgets/PenToolWidget.ts +18 -1
- package/src/tools/Pen.test.ts +150 -0
- package/src/tools/SelectionTool/SelectionTool.css +1 -1
- package/src/tools/SelectionTool/SelectionTool.ts +9 -0
- package/src/tools/ToolController.ts +2 -0
- package/src/tools/ToolbarShortcutHandler.ts +34 -0
- package/src/tools/UndoRedoShortcut.test.ts +3 -0
- package/src/tools/lib.ts +1 -0
- package/src/types.ts +13 -8
- package/tsconfig.json +3 -1
package/src/Editor.ts
CHANGED
@@ -586,6 +586,7 @@ export class Editor {
|
|
586
586
|
kind: InputEvtType.KeyPressEvent,
|
587
587
|
key: evt.key,
|
588
588
|
ctrlKey: evt.ctrlKey,
|
589
|
+
altKey: evt.altKey,
|
589
590
|
})) {
|
590
591
|
evt.preventDefault();
|
591
592
|
} else if (evt.key === 'Escape') {
|
@@ -598,6 +599,7 @@ export class Editor {
|
|
598
599
|
kind: InputEvtType.KeyUpEvent,
|
599
600
|
key: evt.key,
|
600
601
|
ctrlKey: evt.ctrlKey,
|
602
|
+
altKey: evt.altKey,
|
601
603
|
})) {
|
602
604
|
evt.preventDefault();
|
603
605
|
}
|
@@ -783,12 +785,14 @@ export class Editor {
|
|
783
785
|
public sendKeyboardEvent(
|
784
786
|
eventType: InputEvtType.KeyPressEvent|InputEvtType.KeyUpEvent,
|
785
787
|
key: string,
|
786
|
-
ctrlKey: boolean = false
|
788
|
+
ctrlKey: boolean = false,
|
789
|
+
altKey: boolean = false,
|
787
790
|
) {
|
788
791
|
this.toolController.dispatchInputEvent({
|
789
792
|
kind: eventType,
|
790
793
|
key,
|
791
|
-
ctrlKey
|
794
|
+
ctrlKey,
|
795
|
+
altKey,
|
792
796
|
});
|
793
797
|
}
|
794
798
|
|
@@ -797,6 +801,8 @@ export class Editor {
|
|
797
801
|
public sendPenEvent(
|
798
802
|
eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,
|
799
803
|
point: Point2,
|
804
|
+
|
805
|
+
// @deprecated
|
800
806
|
allPointers?: Pointer[]
|
801
807
|
) {
|
802
808
|
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([{
|
package/src/components/Stroke.ts
CHANGED
@@ -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 =
|
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
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
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;
|
package/src/components/Text.ts
CHANGED
@@ -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
|
-
|
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 {
|
@@ -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);
|
@@ -440,16 +433,16 @@ export default class FreehandLineBuilder implements ComponentBuilder {
|
|
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;
|
@@ -2,5 +2,5 @@
|
|
2
2
|
// Compile-time assertion that a branch of code is unreachable.
|
3
3
|
// See https://stackoverflow.com/a/39419171/17055750
|
4
4
|
export const assertUnreachable = (key: never): never => {
|
5
|
-
|
6
|
-
};
|
5
|
+
throw new Error(`Should be unreachable. Key: ${key}.`);
|
6
|
+
};
|
@@ -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
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
@@ -196,7 +196,7 @@ export default class Vec3 {
|
|
196
196
|
* Vec3.of(1, 2, 3).eq(Vec3.of(4, 5, 6), 2.99); // → false
|
197
197
|
* ```
|
198
198
|
*/
|
199
|
-
public eq(other: Vec3, fuzz: number): boolean {
|
199
|
+
public eq(other: Vec3, fuzz: number = 1e-10): boolean {
|
200
200
|
for (let i = 0; i < 3; i++) {
|
201
201
|
if (Math.abs(other.at(i) - this.at(i)) > fuzz) {
|
202
202
|
return false;
|
@@ -88,7 +88,7 @@ export default class SVGRenderer extends AbstractRenderer {
|
|
88
88
|
|
89
89
|
public drawPath(pathSpec: RenderablePathSpec) {
|
90
90
|
const style = pathSpec.style;
|
91
|
-
const path = Path.fromRenderable(pathSpec);
|
91
|
+
const path = Path.fromRenderable(pathSpec).transformedBy(this.getCanvasToScreenTransform());
|
92
92
|
|
93
93
|
// Try to extend the previous path, if possible
|
94
94
|
if (!style.fill.eq(this.lastPathStyle?.fill) || this.lastPathString.length === 0) {
|
@@ -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
|
+
}
|
@@ -22,19 +22,4 @@ export const loadExpectExtensions = () => {
|
|
22
22
|
});
|
23
23
|
};
|
24
24
|
|
25
|
-
// Type declarations for custom matchers
|
26
|
-
export interface CustomMatchers<R = unknown> {
|
27
|
-
objEq(expected: {
|
28
|
-
eq: (other: any, ...args: any)=> boolean;
|
29
|
-
}, ...opts: any): R;
|
30
|
-
}
|
31
|
-
|
32
|
-
declare global {
|
33
|
-
export namespace jest {
|
34
|
-
interface Expect extends CustomMatchers {}
|
35
|
-
interface Matchers<R> extends CustomMatchers<R> {}
|
36
|
-
interface AsyncAsymmetricMatchers extends CustomMatchers {}
|
37
|
-
}
|
38
|
-
}
|
39
|
-
|
40
25
|
export default loadExpectExtensions;
|
package/src/toolbar/toolbar.css
CHANGED
@@ -34,7 +34,8 @@
|
|
34
34
|
}
|
35
35
|
|
36
36
|
.toolbar-button.disabled {
|
37
|
-
filter: opacity(0.
|
37
|
+
filter: opacity(0.5) sepia(0.2);
|
38
|
+
cursor: unset;
|
38
39
|
}
|
39
40
|
|
40
41
|
.toolbar-button, .toolbar-root button {
|
@@ -75,7 +76,7 @@
|
|
75
76
|
width: 6em;
|
76
77
|
}
|
77
78
|
|
78
|
-
.toolbar-button:hover, .toolbar-root button:not(:disabled):hover {
|
79
|
+
.toolbar-button:not(.disabled):hover, .toolbar-root button:not(:disabled):hover {
|
79
80
|
box-shadow: 0px 2px 4px var(--primary-shadow-color);
|
80
81
|
}
|
81
82
|
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import Editor from '../../Editor';
|
2
|
-
import
|
2
|
+
import ToolbarShortcutHandler from '../../tools/ToolbarShortcutHandler';
|
3
|
+
import { EditorEventType, InputEvtType, KeyPressEvent } from '../../types';
|
3
4
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
4
5
|
import { makeDropdownIcon } from '../icons';
|
5
6
|
import { ToolbarLocalization } from '../localization';
|
@@ -33,6 +34,14 @@ export default abstract class BaseWidget {
|
|
33
34
|
this.label = document.createElement('label');
|
34
35
|
this.button.setAttribute('role', 'button');
|
35
36
|
this.button.tabIndex = 0;
|
37
|
+
|
38
|
+
const toolbarShortcutHandlers = this.editor.toolController.getMatchingTools(ToolbarShortcutHandler);
|
39
|
+
|
40
|
+
// If the onKeyPress function has been extended and the editor is configured to send keypress events to
|
41
|
+
// toolbar widgets,
|
42
|
+
if (toolbarShortcutHandlers.length > 0 && this.onKeyPress !== BaseWidget.prototype.onKeyPress) {
|
43
|
+
toolbarShortcutHandlers[0].registerListener(event => this.onKeyPress(event));
|
44
|
+
}
|
36
45
|
}
|
37
46
|
|
38
47
|
protected abstract getTitle(): string;
|
@@ -70,6 +79,7 @@ export default abstract class BaseWidget {
|
|
70
79
|
kind: InputEvtType.KeyPressEvent,
|
71
80
|
key: evt.key,
|
72
81
|
ctrlKey: evt.ctrlKey,
|
82
|
+
altKey: evt.altKey,
|
73
83
|
});
|
74
84
|
}
|
75
85
|
};
|
@@ -83,6 +93,7 @@ export default abstract class BaseWidget {
|
|
83
93
|
kind: InputEvtType.KeyUpEvent,
|
84
94
|
key: evt.key,
|
85
95
|
ctrlKey: evt.ctrlKey,
|
96
|
+
altKey: evt.altKey,
|
86
97
|
});
|
87
98
|
};
|
88
99
|
|
@@ -93,6 +104,13 @@ export default abstract class BaseWidget {
|
|
93
104
|
};
|
94
105
|
}
|
95
106
|
|
107
|
+
// Add a listener that is triggered when a key is pressed.
|
108
|
+
// Listeners will fire regardless of whether this widget is selected and require that
|
109
|
+
// {@link lib!Editor.toolController} to have an enabled {@link lib!ToolbarShortcutHandler} tool.
|
110
|
+
protected onKeyPress(_event: KeyPressEvent): boolean {
|
111
|
+
return false;
|
112
|
+
}
|
113
|
+
|
96
114
|
protected abstract handleClick(): void;
|
97
115
|
|
98
116
|
protected get hasDropdown() {
|
@@ -5,7 +5,7 @@ import { makeFilledRectangleBuilder, makeOutlinedRectangleBuilder } from '../../
|
|
5
5
|
import { ComponentBuilderFactory } from '../../components/builders/types';
|
6
6
|
import Editor from '../../Editor';
|
7
7
|
import Pen from '../../tools/Pen';
|
8
|
-
import { EditorEventType } from '../../types';
|
8
|
+
import { EditorEventType, KeyPressEvent } from '../../types';
|
9
9
|
import { toolbarCSSPrefix } from '../HTMLToolbar';
|
10
10
|
import { makeIconFromFactory, makePenIcon } from '../icons';
|
11
11
|
import { ToolbarLocalization } from '../localization';
|
@@ -165,4 +165,21 @@ export default class PenToolWidget extends BaseToolWidget {
|
|
165
165
|
dropdown.replaceChildren(container);
|
166
166
|
return true;
|
167
167
|
}
|
168
|
+
|
169
|
+
protected onKeyPress(event: KeyPressEvent): boolean {
|
170
|
+
if (!this.isSelected()) {
|
171
|
+
return false;
|
172
|
+
}
|
173
|
+
|
174
|
+
// Map alt+0-9 to different pen types.
|
175
|
+
if (/^[0-9]$/.exec(event.key) && event.ctrlKey) {
|
176
|
+
const penTypeIdx = parseInt(event.key) - 1;
|
177
|
+
if (penTypeIdx >= 0 && penTypeIdx < this.penTypes.length) {
|
178
|
+
this.tool.setStrokeFactory(this.penTypes[penTypeIdx].factory);
|
179
|
+
return true;
|
180
|
+
}
|
181
|
+
}
|
182
|
+
|
183
|
+
return false;
|
184
|
+
}
|
168
185
|
}
|