js-draw 0.3.2 → 0.4.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/CHANGELOG.md +9 -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 +104 -76
- 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/ImageComponent.d.ts +2 -2
- 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 +11 -1
- package/dist/src/math/Vec3.js +15 -0
- 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/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 +276 -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/package.json +1 -1
- package/src/Editor.css +1 -0
- package/src/Editor.ts +145 -108
- package/src/Pointer.ts +8 -3
- package/src/Viewport.ts +17 -2
- package/src/components/AbstractComponent.ts +2 -6
- package/src/components/ImageComponent.ts +2 -6
- package/src/components/Text.ts +2 -6
- package/src/language/assertions.ts +6 -0
- package/src/math/Mat33.test.ts +14 -0
- 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/Vec3.ts +22 -1
- 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/toolbar/HTMLToolbar.ts +5 -4
- package/src/toolbar/widgets/SelectionToolWidget.ts +1 -1
- package/src/tools/PasteHandler.ts +4 -1
- 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 +335 -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/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,6 +778,20 @@ 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(
|
@@ -864,7 +901,7 @@ export class Editor {
|
|
864
901
|
|
865
902
|
/**
|
866
903
|
* Alias for loadFrom(SVGLoader.fromString).
|
867
|
-
*
|
904
|
+
*
|
868
905
|
* This is particularly useful when accessing a bundled version of the editor,
|
869
906
|
* where `SVGLoader.fromString` is unavailable.
|
870
907
|
*/
|
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);
|
@@ -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
|
}
|
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';
|
@@ -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);
|
package/src/math/Mat33.test.ts
CHANGED
@@ -142,6 +142,20 @@ describe('Mat33 tests', () => {
|
|
142
142
|
).objEq(Vec2.unitX, fuzz);
|
143
143
|
});
|
144
144
|
|
145
|
+
it('should correctly apply a mapping to all components', () => {
|
146
|
+
expect(
|
147
|
+
new Mat33(
|
148
|
+
1, 2, 3,
|
149
|
+
4, 5, 6,
|
150
|
+
7, 8, 9,
|
151
|
+
).mapEntries(component => component - 1)
|
152
|
+
).toMatchObject(new Mat33(
|
153
|
+
0, 1, 2,
|
154
|
+
3, 4, 5,
|
155
|
+
6, 7, 8,
|
156
|
+
));
|
157
|
+
});
|
158
|
+
|
145
159
|
it('should convert CSS matrix(...) strings to matricies', () => {
|
146
160
|
// From MDN:
|
147
161
|
// ⎡ 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
|
}
|