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
package/src/Editor.ts
CHANGED
@@ -1,18 +1,18 @@
|
|
1
1
|
/**
|
2
2
|
* The main entrypoint for the full editor.
|
3
|
-
*
|
3
|
+
*
|
4
4
|
* @example
|
5
5
|
* To create an editor with a toolbar,
|
6
6
|
* ```
|
7
7
|
* const editor = new Editor(document.body);
|
8
|
-
*
|
8
|
+
*
|
9
9
|
* const toolbar = editor.addToolbar();
|
10
10
|
* toolbar.addActionButton('Save', () => {
|
11
11
|
* const saveData = editor.toSVG().outerHTML;
|
12
12
|
* // Do something with saveData...
|
13
13
|
* });
|
14
14
|
* ```
|
15
|
-
*
|
15
|
+
*
|
16
16
|
* @packageDocumentation
|
17
17
|
*/
|
18
18
|
|
@@ -38,6 +38,9 @@ import Rect2 from './math/Rect2';
|
|
38
38
|
import { EditorLocalization } from './localization';
|
39
39
|
import getLocalizationTable from './localizations/getLocalizationTable';
|
40
40
|
|
41
|
+
type HTMLPointerEventType = 'pointerdown'|'pointermove'|'pointerup'|'pointercancel';
|
42
|
+
type HTMLPointerEventFilter = (eventName: HTMLPointerEventType, event: PointerEvent)=>boolean;
|
43
|
+
|
41
44
|
export interface EditorSettings {
|
42
45
|
/** Defaults to `RenderingMode.CanvasRenderer` */
|
43
46
|
renderingMode: RenderingMode,
|
@@ -67,14 +70,14 @@ export class Editor {
|
|
67
70
|
|
68
71
|
/**
|
69
72
|
* Handles undo/redo.
|
70
|
-
*
|
73
|
+
*
|
71
74
|
* @example
|
72
75
|
* ```
|
73
76
|
* const editor = new Editor(document.body);
|
74
|
-
*
|
77
|
+
*
|
75
78
|
* // Do something undoable.
|
76
79
|
* // ...
|
77
|
-
*
|
80
|
+
*
|
78
81
|
* // Undo the last action
|
79
82
|
* editor.history.undo();
|
80
83
|
* ```
|
@@ -83,17 +86,17 @@ export class Editor {
|
|
83
86
|
|
84
87
|
/**
|
85
88
|
* Data structure for adding/removing/querying objects in the image.
|
86
|
-
*
|
89
|
+
*
|
87
90
|
* @example
|
88
91
|
* ```
|
89
92
|
* const editor = new Editor(document.body);
|
90
|
-
*
|
93
|
+
*
|
91
94
|
* // Create a path.
|
92
95
|
* const stroke = new Stroke([
|
93
96
|
* Path.fromString('M0,0 L30,30 z').toRenderable({ fill: Color4.black }),
|
94
97
|
* ]);
|
95
98
|
* const addElementCommand = editor.image.addElement(stroke);
|
96
|
-
*
|
99
|
+
*
|
97
100
|
* // Add the stroke to the editor
|
98
101
|
* editor.dispatch(addElementCommand);
|
99
102
|
* ```
|
@@ -126,14 +129,14 @@ export class Editor {
|
|
126
129
|
* @example
|
127
130
|
* ```
|
128
131
|
* const container = document.body;
|
129
|
-
*
|
132
|
+
*
|
130
133
|
* // Create an editor
|
131
134
|
* const editor = new Editor(container, {
|
132
135
|
* // 2e-10 and 1e12 are the default values for minimum/maximum zoom.
|
133
136
|
* minZoom: 2e-10,
|
134
137
|
* maxZoom: 1e12,
|
135
138
|
* });
|
136
|
-
*
|
139
|
+
*
|
137
140
|
* // Add the default toolbar
|
138
141
|
* const toolbar = editor.addToolbar();
|
139
142
|
* toolbar.addActionButton({
|
@@ -224,7 +227,7 @@ export class Editor {
|
|
224
227
|
if (oldZoom <= this.settings.maxZoom && oldZoom >= this.settings.minZoom) {
|
225
228
|
resetTransform = evt.oldTransform;
|
226
229
|
}
|
227
|
-
|
230
|
+
|
228
231
|
this.viewport.resetTransform(resetTransform);
|
229
232
|
}
|
230
233
|
}
|
@@ -233,7 +236,7 @@ export class Editor {
|
|
233
236
|
|
234
237
|
/**
|
235
238
|
* @returns a reference to the editor's container.
|
236
|
-
*
|
239
|
+
*
|
237
240
|
* @example
|
238
241
|
* ```
|
239
242
|
* editor.getRootElement().style.height = '500px';
|
@@ -285,96 +288,7 @@ export class Editor {
|
|
285
288
|
}
|
286
289
|
|
287
290
|
private registerListeners() {
|
288
|
-
|
289
|
-
const getPointerList = () => {
|
290
|
-
const nowTime = (new Date()).getTime();
|
291
|
-
|
292
|
-
const res: Pointer[] = [];
|
293
|
-
for (const id in pointers) {
|
294
|
-
const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
|
295
|
-
if (pointers[id] && (nowTime - pointers[id].timeStamp) < maxUnupdatedTime) {
|
296
|
-
res.push(pointers[id]);
|
297
|
-
}
|
298
|
-
}
|
299
|
-
return res;
|
300
|
-
};
|
301
|
-
|
302
|
-
// May be required to prevent text selection on iOS/Safari:
|
303
|
-
// See https://stackoverflow.com/a/70992717/17055750
|
304
|
-
this.renderingRegion.addEventListener('touchstart', evt => evt.preventDefault());
|
305
|
-
this.renderingRegion.addEventListener('contextmenu', evt => {
|
306
|
-
// Don't show a context menu
|
307
|
-
evt.preventDefault();
|
308
|
-
});
|
309
|
-
|
310
|
-
this.renderingRegion.addEventListener('pointerdown', evt => {
|
311
|
-
const pointer = Pointer.ofEvent(evt, true, this.viewport);
|
312
|
-
pointers[pointer.id] = pointer;
|
313
|
-
|
314
|
-
this.renderingRegion.setPointerCapture(pointer.id);
|
315
|
-
const event: PointerEvt = {
|
316
|
-
kind: InputEvtType.PointerDownEvt,
|
317
|
-
current: pointer,
|
318
|
-
allPointers: getPointerList(),
|
319
|
-
};
|
320
|
-
this.toolController.dispatchInputEvent(event);
|
321
|
-
|
322
|
-
return true;
|
323
|
-
});
|
324
|
-
|
325
|
-
this.renderingRegion.addEventListener('pointermove', evt => {
|
326
|
-
const pointer = Pointer.ofEvent(
|
327
|
-
evt, pointers[evt.pointerId]?.down ?? false, this.viewport
|
328
|
-
);
|
329
|
-
if (pointer.down) {
|
330
|
-
const prevData = pointers[pointer.id];
|
331
|
-
|
332
|
-
if (prevData) {
|
333
|
-
const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
|
334
|
-
|
335
|
-
// If the pointer moved less than two pixels, don't send a new event.
|
336
|
-
if (distanceMoved < 2) {
|
337
|
-
return;
|
338
|
-
}
|
339
|
-
}
|
340
|
-
|
341
|
-
pointers[pointer.id] = pointer;
|
342
|
-
if (this.toolController.dispatchInputEvent({
|
343
|
-
kind: InputEvtType.PointerMoveEvt,
|
344
|
-
current: pointer,
|
345
|
-
allPointers: getPointerList(),
|
346
|
-
})) {
|
347
|
-
evt.preventDefault();
|
348
|
-
}
|
349
|
-
}
|
350
|
-
});
|
351
|
-
|
352
|
-
const pointerEnd = (evt: PointerEvent) => {
|
353
|
-
const pointer = Pointer.ofEvent(evt, false, this.viewport);
|
354
|
-
if (!pointers[pointer.id]) {
|
355
|
-
return;
|
356
|
-
}
|
357
|
-
|
358
|
-
pointers[pointer.id] = pointer;
|
359
|
-
this.renderingRegion.releasePointerCapture(pointer.id);
|
360
|
-
if (this.toolController.dispatchInputEvent({
|
361
|
-
kind: InputEvtType.PointerUpEvt,
|
362
|
-
current: pointer,
|
363
|
-
allPointers: getPointerList(),
|
364
|
-
})) {
|
365
|
-
evt.preventDefault();
|
366
|
-
}
|
367
|
-
delete pointers[pointer.id];
|
368
|
-
};
|
369
|
-
|
370
|
-
this.renderingRegion.addEventListener('pointerup', evt => {
|
371
|
-
pointerEnd(evt);
|
372
|
-
});
|
373
|
-
|
374
|
-
this.renderingRegion.addEventListener('pointercancel', evt => {
|
375
|
-
pointerEnd(evt);
|
376
|
-
});
|
377
|
-
|
291
|
+
this.handlePointerEventsFrom(this.renderingRegion);
|
378
292
|
this.handleKeyEventsFrom(this.renderingRegion);
|
379
293
|
|
380
294
|
this.container.addEventListener('wheel', evt => {
|
@@ -404,7 +318,10 @@ export class Editor {
|
|
404
318
|
delta = Vec3.of(0, 0, evt.deltaY);
|
405
319
|
}
|
406
320
|
|
407
|
-
|
321
|
+
// Ensure that `pos` is relative to `this.container`
|
322
|
+
const bbox = this.container.getBoundingClientRect();
|
323
|
+
const pos = Vec2.of(evt.clientX, evt.clientY).minus(Vec2.of(bbox.left, bbox.top));
|
324
|
+
|
408
325
|
if (this.toolController.dispatchInputEvent({
|
409
326
|
kind: InputEvtType.WheelEvt,
|
410
327
|
delta,
|
@@ -459,6 +376,91 @@ export class Editor {
|
|
459
376
|
});
|
460
377
|
}
|
461
378
|
|
379
|
+
private pointers: Record<number, Pointer> = {};
|
380
|
+
private getPointerList() {
|
381
|
+
const nowTime = (new Date()).getTime();
|
382
|
+
|
383
|
+
const res: Pointer[] = [];
|
384
|
+
for (const id in this.pointers) {
|
385
|
+
const maxUnupdatedTime = 2000; // Maximum time without a pointer update (ms)
|
386
|
+
if (this.pointers[id] && (nowTime - this.pointers[id].timeStamp) < maxUnupdatedTime) {
|
387
|
+
res.push(this.pointers[id]);
|
388
|
+
}
|
389
|
+
}
|
390
|
+
return res;
|
391
|
+
}
|
392
|
+
|
393
|
+
/**
|
394
|
+
* Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
|
395
|
+
* as the content of the editor.
|
396
|
+
*/
|
397
|
+
public handleHTMLPointerEvent(eventType: 'pointerdown'|'pointermove'|'pointerup'|'pointercancel', evt: PointerEvent): boolean {
|
398
|
+
const eventsRelativeTo = this.renderingRegion;
|
399
|
+
const eventTarget = (evt.target as HTMLElement|null) ?? this.renderingRegion;
|
400
|
+
|
401
|
+
if (eventType === 'pointerdown') {
|
402
|
+
const pointer = Pointer.ofEvent(evt, true, this.viewport, eventsRelativeTo);
|
403
|
+
this.pointers[pointer.id] = pointer;
|
404
|
+
|
405
|
+
eventTarget.setPointerCapture(pointer.id);
|
406
|
+
const event: PointerEvt = {
|
407
|
+
kind: InputEvtType.PointerDownEvt,
|
408
|
+
current: pointer,
|
409
|
+
allPointers: this.getPointerList(),
|
410
|
+
};
|
411
|
+
this.toolController.dispatchInputEvent(event);
|
412
|
+
|
413
|
+
return true;
|
414
|
+
}
|
415
|
+
else if (eventType === 'pointermove') {
|
416
|
+
const pointer = Pointer.ofEvent(
|
417
|
+
evt, this.pointers[evt.pointerId]?.down ?? false, this.viewport, eventsRelativeTo
|
418
|
+
);
|
419
|
+
if (pointer.down) {
|
420
|
+
const prevData = this.pointers[pointer.id];
|
421
|
+
|
422
|
+
if (prevData) {
|
423
|
+
const distanceMoved = pointer.screenPos.minus(prevData.screenPos).magnitude();
|
424
|
+
|
425
|
+
// If the pointer moved less than two pixels, don't send a new event.
|
426
|
+
if (distanceMoved < 2) {
|
427
|
+
return false;
|
428
|
+
}
|
429
|
+
}
|
430
|
+
|
431
|
+
this.pointers[pointer.id] = pointer;
|
432
|
+
if (this.toolController.dispatchInputEvent({
|
433
|
+
kind: InputEvtType.PointerMoveEvt,
|
434
|
+
current: pointer,
|
435
|
+
allPointers: this.getPointerList(),
|
436
|
+
})) {
|
437
|
+
evt.preventDefault();
|
438
|
+
}
|
439
|
+
}
|
440
|
+
return true;
|
441
|
+
}
|
442
|
+
else if (eventType === 'pointercancel' || eventType === 'pointerup') {
|
443
|
+
const pointer = Pointer.ofEvent(evt, false, this.viewport, eventsRelativeTo);
|
444
|
+
if (!this.pointers[pointer.id]) {
|
445
|
+
return false;
|
446
|
+
}
|
447
|
+
|
448
|
+
this.pointers[pointer.id] = pointer;
|
449
|
+
eventTarget.releasePointerCapture(pointer.id);
|
450
|
+
if (this.toolController.dispatchInputEvent({
|
451
|
+
kind: InputEvtType.PointerUpEvt,
|
452
|
+
current: pointer,
|
453
|
+
allPointers: this.getPointerList(),
|
454
|
+
})) {
|
455
|
+
evt.preventDefault();
|
456
|
+
}
|
457
|
+
delete this.pointers[pointer.id];
|
458
|
+
return true;
|
459
|
+
}
|
460
|
+
|
461
|
+
return eventType;
|
462
|
+
}
|
463
|
+
|
462
464
|
private isEventSink(evtTarget: Element|EventTarget|null) {
|
463
465
|
let currentElem: Element|null = evtTarget as Element|null;
|
464
466
|
while (currentElem !== null) {
|
@@ -553,6 +555,27 @@ export class Editor {
|
|
553
555
|
}
|
554
556
|
}
|
555
557
|
|
558
|
+
public handlePointerEventsFrom(elem: HTMLElement, filter?: HTMLPointerEventFilter) {
|
559
|
+
// May be required to prevent text selection on iOS/Safari:
|
560
|
+
// See https://stackoverflow.com/a/70992717/17055750
|
561
|
+
elem.addEventListener('touchstart', evt => evt.preventDefault());
|
562
|
+
elem.addEventListener('contextmenu', evt => {
|
563
|
+
// Don't show a context menu
|
564
|
+
evt.preventDefault();
|
565
|
+
});
|
566
|
+
|
567
|
+
const eventNames: HTMLPointerEventType[] = ['pointerdown', 'pointermove', 'pointerup', 'pointercancel'];
|
568
|
+
for (const eventName of eventNames) {
|
569
|
+
elem.addEventListener(eventName, evt => {
|
570
|
+
if (filter && !filter(eventName, evt)) {
|
571
|
+
return true;
|
572
|
+
}
|
573
|
+
|
574
|
+
return this.handleHTMLPointerEvent(eventName, evt);
|
575
|
+
});
|
576
|
+
}
|
577
|
+
}
|
578
|
+
|
556
579
|
/** Adds event listners for keypresses to `elem` and forwards those events to the editor. */
|
557
580
|
public handleKeyEventsFrom(elem: HTMLElement) {
|
558
581
|
elem.addEventListener('keydown', evt => {
|
@@ -567,7 +590,7 @@ export class Editor {
|
|
567
590
|
evt.preventDefault();
|
568
591
|
} else if (evt.key === 'Escape') {
|
569
592
|
this.renderingRegion.blur();
|
570
|
-
}
|
593
|
+
}
|
571
594
|
});
|
572
595
|
|
573
596
|
elem.addEventListener('keyup', evt => {
|
@@ -609,11 +632,11 @@ export class Editor {
|
|
609
632
|
* Dispatches a command without announcing it. By default, does not add to history.
|
610
633
|
* Use this to show finalized commands that don't need to have `announceForAccessibility`
|
611
634
|
* called.
|
612
|
-
*
|
635
|
+
*
|
613
636
|
* Prefer `command.apply(editor)` for incomplete commands. `dispatchNoAnnounce` may allow
|
614
637
|
* clients to listen for the application of commands (e.g. `SerializableCommand`s so they can
|
615
638
|
* be sent across the network), while `apply` does not.
|
616
|
-
*
|
639
|
+
*
|
617
640
|
* @example
|
618
641
|
* ```
|
619
642
|
* const addToHistory = false;
|
@@ -755,11 +778,27 @@ export class Editor {
|
|
755
778
|
return styleSheet;
|
756
779
|
}
|
757
780
|
|
781
|
+
// Dispatch a keyboard event to the currently selected tool.
|
782
|
+
// Intended for unit testing
|
783
|
+
public sendKeyboardEvent(
|
784
|
+
eventType: InputEvtType.KeyPressEvent|InputEvtType.KeyUpEvent,
|
785
|
+
key: string,
|
786
|
+
ctrlKey: boolean = false
|
787
|
+
) {
|
788
|
+
this.toolController.dispatchInputEvent({
|
789
|
+
kind: eventType,
|
790
|
+
key,
|
791
|
+
ctrlKey
|
792
|
+
});
|
793
|
+
}
|
794
|
+
|
758
795
|
// Dispatch a pen event to the currently selected tool.
|
759
796
|
// Intended primarially for unit tests.
|
760
797
|
public sendPenEvent(
|
761
798
|
eventType: InputEvtType.PointerDownEvt|InputEvtType.PointerMoveEvt|InputEvtType.PointerUpEvt,
|
762
799
|
point: Point2,
|
800
|
+
|
801
|
+
// @deprecated
|
763
802
|
allPointers?: Pointer[]
|
764
803
|
) {
|
765
804
|
const mainPointer = Pointer.ofCanvasPoint(
|
@@ -864,7 +903,7 @@ export class Editor {
|
|
864
903
|
|
865
904
|
/**
|
866
905
|
* Alias for loadFrom(SVGLoader.fromString).
|
867
|
-
*
|
906
|
+
*
|
868
907
|
* This is particularly useful when accessing a bundled version of the editor,
|
869
908
|
* where `SVGLoader.fromString` is unavailable.
|
870
909
|
*/
|
package/src/Pointer.ts
CHANGED
@@ -36,9 +36,14 @@ export default class Pointer {
|
|
36
36
|
) {
|
37
37
|
}
|
38
38
|
|
39
|
-
// Creates a Pointer from a DOM event.
|
40
|
-
|
41
|
-
|
39
|
+
// Creates a Pointer from a DOM event. If `relativeTo` is given, (0, 0) in screen coordinates is
|
40
|
+
// considered the top left of `relativeTo`.
|
41
|
+
public static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport, relativeTo?: HTMLElement): Pointer {
|
42
|
+
let screenPos = Vec2.of(evt.clientX, evt.clientY);
|
43
|
+
if (relativeTo) {
|
44
|
+
const bbox = relativeTo.getBoundingClientRect();
|
45
|
+
screenPos = screenPos.minus(Vec2.of(bbox.left, bbox.top));
|
46
|
+
}
|
42
47
|
|
43
48
|
const pointerTypeToDevice: Record<string, PointerDevice> = {
|
44
49
|
'mouse': PointerDevice.PrimaryButtonMouse,
|
package/src/Viewport.ts
CHANGED
@@ -146,7 +146,8 @@ export class Viewport {
|
|
146
146
|
return 1/this.getScaleFactor();
|
147
147
|
}
|
148
148
|
|
149
|
-
// Returns the angle of the canvas in radians
|
149
|
+
// Returns the angle of the canvas in radians.
|
150
|
+
// This is the angle by which the canvas is rotated relative to the screen.
|
150
151
|
public getRotationAngle(): number {
|
151
152
|
return this.transform.transformVec3(Vec3.unitX).angle();
|
152
153
|
}
|
@@ -175,12 +176,26 @@ export class Viewport {
|
|
175
176
|
return point.map(roundComponent);
|
176
177
|
}
|
177
178
|
|
178
|
-
|
179
179
|
// Round a point with a tolerance of ±1 screen unit.
|
180
180
|
public roundPoint(point: Point2): Point2 {
|
181
181
|
return Viewport.roundPoint(point, 1 / this.getScaleFactor());
|
182
182
|
}
|
183
183
|
|
184
|
+
// `roundAmount`: An integer >= 0, larger numbers cause less rounding. Smaller numbers cause more
|
185
|
+
// (as such `roundAmount = 0` does the most rounding).
|
186
|
+
public static roundScaleRatio(scaleRatio: number, roundAmount: number = 1): number {
|
187
|
+
if (Math.abs(scaleRatio) <= 1e-12) {
|
188
|
+
return 0;
|
189
|
+
}
|
190
|
+
|
191
|
+
// Represent as k 10ⁿ for some n, k ∈ ℤ.
|
192
|
+
const decimalComponent = 10 ** Math.floor(Math.log10(Math.abs(scaleRatio)));
|
193
|
+
const roundAnountFactor = 2 ** roundAmount;
|
194
|
+
scaleRatio = Math.round(scaleRatio / decimalComponent * roundAnountFactor) / roundAnountFactor * decimalComponent;
|
195
|
+
|
196
|
+
return scaleRatio;
|
197
|
+
}
|
198
|
+
|
184
199
|
// Computes and returns an affine transformation that makes `toMakeVisible` visible and roughly centered on the screen.
|
185
200
|
public computeZoomToTransform(toMakeVisible: Rect2, allowZoomIn: boolean = true, allowZoomOut: boolean = true): Mat33 {
|
186
201
|
let transform = Mat33.identity;
|
@@ -2,7 +2,7 @@ import SerializableCommand from '../commands/SerializableCommand';
|
|
2
2
|
import Editor from '../Editor';
|
3
3
|
import EditorImage from '../EditorImage';
|
4
4
|
import LineSegment2 from '../math/LineSegment2';
|
5
|
-
import Mat33 from '../math/Mat33';
|
5
|
+
import Mat33, { Mat33Array } from '../math/Mat33';
|
6
6
|
import Rect2 from '../math/Rect2';
|
7
7
|
import { EditorLocalization } from '../localization';
|
8
8
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
@@ -183,11 +183,7 @@ export default abstract class AbstractComponent {
|
|
183
183
|
SerializableCommand.register(AbstractComponent.transformElementCommandId, (json: any, editor: Editor) => {
|
184
184
|
const elem = editor.image.lookupElement(json.id);
|
185
185
|
|
186
|
-
const transform = new Mat33(...(json.transfm as
|
187
|
-
number, number, number,
|
188
|
-
number, number, number,
|
189
|
-
number, number, number,
|
190
|
-
]));
|
186
|
+
const transform = new Mat33(...(json.transfm as Mat33Array));
|
191
187
|
|
192
188
|
if (!elem) {
|
193
189
|
return new AbstractComponent.UnresolvedTransformElementCommand(transform, json.id);
|
@@ -210,8 +206,10 @@ export default abstract class AbstractComponent {
|
|
210
206
|
|
211
207
|
public abstract description(localizationTable: ImageComponentLocalization): string;
|
212
208
|
|
209
|
+
// Component-specific implementation of {@link clone}.
|
213
210
|
protected abstract createClone(): AbstractComponent;
|
214
211
|
|
212
|
+
// Returns a copy of this component.
|
215
213
|
public clone() {
|
216
214
|
const clone = this.createClone();
|
217
215
|
|
@@ -1,5 +1,5 @@
|
|
1
1
|
import LineSegment2 from '../math/LineSegment2';
|
2
|
-
import Mat33 from '../math/Mat33';
|
2
|
+
import Mat33, { Mat33Array } from '../math/Mat33';
|
3
3
|
import Rect2 from '../math/Rect2';
|
4
4
|
import AbstractRenderer, { RenderableImage } from '../rendering/renderers/AbstractRenderer';
|
5
5
|
import AbstractComponent from './AbstractComponent';
|
@@ -141,11 +141,7 @@ export default class ImageComponent extends AbstractComponent {
|
|
141
141
|
image: image,
|
142
142
|
base64Url: image.src,
|
143
143
|
label: data.label,
|
144
|
-
transform: new Mat33(...(data.transform as
|
145
|
-
number, number, number,
|
146
|
-
number, number, number,
|
147
|
-
number, number, number,
|
148
|
-
])),
|
144
|
+
transform: new Mat33(...(data.transform as Mat33Array)),
|
149
145
|
});
|
150
146
|
}
|
151
147
|
}
|
@@ -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
@@ -1,5 +1,5 @@
|
|
1
1
|
import LineSegment2 from '../math/LineSegment2';
|
2
|
-
import Mat33 from '../math/Mat33';
|
2
|
+
import Mat33, { Mat33Array } from '../math/Mat33';
|
3
3
|
import Rect2 from '../math/Rect2';
|
4
4
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
5
5
|
import RenderingStyle, { styleFromJSON, styleToJSON } from '../rendering/RenderingStyle';
|
@@ -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 {
|
@@ -200,11 +200,7 @@ export default class Text extends AbstractComponent {
|
|
200
200
|
throw new Error(`Unable to deserialize transform, ${json.transform}.`);
|
201
201
|
}
|
202
202
|
|
203
|
-
const transformData = json.transform as
|
204
|
-
number, number, number,
|
205
|
-
number, number, number,
|
206
|
-
number, number, number,
|
207
|
-
];
|
203
|
+
const transformData = json.transform as Mat33Array;
|
208
204
|
const transform = new Mat33(...transformData);
|
209
205
|
|
210
206
|
return new Text(textObjects, transform, style, getTextDimens);
|