js-draw 0.0.10 → 0.1.2
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 +11 -0
- package/dist/bundle.js +1 -1
- package/dist/src/Editor.d.ts +2 -2
- package/dist/src/Editor.js +17 -7
- package/dist/src/EditorImage.d.ts +15 -7
- package/dist/src/EditorImage.js +46 -37
- package/dist/src/Pointer.d.ts +3 -2
- package/dist/src/Pointer.js +12 -3
- package/dist/src/SVGLoader.d.ts +6 -2
- package/dist/src/SVGLoader.js +20 -8
- package/dist/src/Viewport.d.ts +4 -0
- package/dist/src/Viewport.js +51 -0
- package/dist/src/components/AbstractComponent.d.ts +9 -2
- package/dist/src/components/AbstractComponent.js +14 -0
- package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -1
- package/dist/src/components/SVGGlobalAttributesObject.js +1 -1
- package/dist/src/components/Stroke.d.ts +1 -1
- package/dist/src/components/Stroke.js +1 -1
- package/dist/src/components/UnknownSVGObject.d.ts +1 -1
- package/dist/src/components/UnknownSVGObject.js +1 -1
- package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
- package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -1
- package/dist/src/components/builders/FreehandLineBuilder.js +1 -1
- package/dist/src/components/builders/LineBuilder.d.ts +1 -1
- package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
- package/dist/src/components/builders/types.d.ts +1 -1
- package/dist/src/geometry/Mat33.js +3 -0
- package/dist/src/geometry/Path.d.ts +1 -1
- package/dist/src/geometry/Path.js +102 -69
- package/dist/src/geometry/Rect2.d.ts +1 -0
- package/dist/src/geometry/Rect2.js +47 -9
- package/dist/src/{Display.d.ts → rendering/Display.d.ts} +5 -2
- package/dist/src/{Display.js → rendering/Display.js} +34 -4
- package/dist/src/rendering/caching/CacheRecord.d.ts +19 -0
- package/dist/src/rendering/caching/CacheRecord.js +52 -0
- package/dist/src/rendering/caching/CacheRecordManager.d.ts +11 -0
- package/dist/src/rendering/caching/CacheRecordManager.js +31 -0
- package/dist/src/rendering/caching/RenderingCache.d.ts +12 -0
- package/dist/src/rendering/caching/RenderingCache.js +42 -0
- package/dist/src/rendering/caching/RenderingCacheNode.d.ts +28 -0
- package/dist/src/rendering/caching/RenderingCacheNode.js +301 -0
- package/dist/src/rendering/caching/testUtils.d.ts +9 -0
- package/dist/src/rendering/caching/testUtils.js +20 -0
- package/dist/src/rendering/caching/types.d.ts +21 -0
- package/dist/src/rendering/caching/types.js +1 -0
- package/dist/src/rendering/{AbstractRenderer.d.ts → renderers/AbstractRenderer.d.ts} +20 -9
- package/dist/src/rendering/{AbstractRenderer.js → renderers/AbstractRenderer.js} +37 -3
- package/dist/src/rendering/{CanvasRenderer.d.ts → renderers/CanvasRenderer.d.ts} +10 -5
- package/dist/src/rendering/{CanvasRenderer.js → renderers/CanvasRenderer.js} +60 -20
- package/dist/src/rendering/{DummyRenderer.d.ts → renderers/DummyRenderer.d.ts} +9 -5
- package/dist/src/rendering/{DummyRenderer.js → renderers/DummyRenderer.js} +35 -4
- package/dist/src/rendering/{SVGRenderer.d.ts → renderers/SVGRenderer.d.ts} +7 -5
- package/dist/src/rendering/{SVGRenderer.js → renderers/SVGRenderer.js} +35 -18
- package/dist/src/testing/createEditor.js +1 -1
- package/dist/src/toolbar/HTMLToolbar.d.ts +2 -1
- package/dist/src/toolbar/HTMLToolbar.js +165 -154
- package/dist/src/toolbar/icons.d.ts +10 -0
- package/dist/src/toolbar/icons.js +180 -0
- package/dist/src/toolbar/localization.d.ts +4 -1
- package/dist/src/toolbar/localization.js +4 -1
- package/dist/src/toolbar/types.d.ts +4 -0
- package/dist/src/tools/PanZoom.d.ts +9 -6
- package/dist/src/tools/PanZoom.js +30 -21
- package/dist/src/tools/Pen.js +8 -3
- package/dist/src/tools/SelectionTool.js +9 -24
- package/dist/src/tools/ToolController.d.ts +5 -6
- package/dist/src/tools/ToolController.js +8 -10
- package/dist/src/tools/localization.d.ts +1 -0
- package/dist/src/tools/localization.js +1 -0
- package/dist/src/types.d.ts +2 -1
- package/package.json +1 -1
- package/src/Editor.ts +19 -8
- package/src/EditorImage.test.ts +2 -2
- package/src/EditorImage.ts +58 -42
- package/src/Pointer.ts +13 -4
- package/src/SVGLoader.ts +36 -10
- package/src/Viewport.ts +68 -0
- package/src/components/AbstractComponent.ts +21 -2
- package/src/components/SVGGlobalAttributesObject.ts +2 -2
- package/src/components/Stroke.ts +2 -2
- package/src/components/UnknownSVGObject.ts +2 -2
- package/src/components/builders/ArrowBuilder.ts +1 -1
- package/src/components/builders/FreehandLineBuilder.ts +2 -2
- package/src/components/builders/LineBuilder.ts +1 -1
- package/src/components/builders/RectangleBuilder.ts +1 -1
- package/src/components/builders/types.ts +1 -1
- package/src/geometry/Mat33.ts +3 -0
- package/src/geometry/Path.fromString.test.ts +94 -4
- package/src/geometry/Path.toString.test.ts +12 -2
- package/src/geometry/Path.ts +107 -71
- package/src/geometry/Rect2.test.ts +47 -8
- package/src/geometry/Rect2.ts +57 -9
- package/src/{Display.ts → rendering/Display.ts} +39 -6
- package/src/rendering/caching/CacheRecord.test.ts +49 -0
- package/src/rendering/caching/CacheRecord.ts +73 -0
- package/src/rendering/caching/CacheRecordManager.ts +45 -0
- package/src/rendering/caching/RenderingCache.test.ts +44 -0
- package/src/rendering/caching/RenderingCache.ts +63 -0
- package/src/rendering/caching/RenderingCacheNode.ts +378 -0
- package/src/rendering/caching/testUtils.ts +35 -0
- package/src/rendering/caching/types.ts +39 -0
- package/src/rendering/{AbstractRenderer.ts → renderers/AbstractRenderer.ts} +57 -9
- package/src/rendering/{CanvasRenderer.ts → renderers/CanvasRenderer.ts} +74 -25
- package/src/rendering/renderers/DummyRenderer.test.ts +43 -0
- package/src/rendering/{DummyRenderer.ts → renderers/DummyRenderer.ts} +50 -7
- package/src/rendering/{SVGRenderer.ts → renderers/SVGRenderer.ts} +39 -23
- package/src/testing/createEditor.ts +1 -1
- package/src/toolbar/HTMLToolbar.ts +199 -170
- package/src/toolbar/icons.ts +203 -0
- package/src/toolbar/localization.ts +9 -2
- package/src/toolbar/toolbar.css +21 -8
- package/src/toolbar/types.ts +5 -0
- package/src/tools/PanZoom.ts +37 -27
- package/src/tools/Pen.ts +7 -3
- package/src/tools/SelectionTool.test.ts +1 -1
- package/src/tools/SelectionTool.ts +12 -33
- package/src/tools/ToolController.ts +3 -5
- package/src/tools/localization.ts +2 -0
- package/src/types.ts +10 -3
- package/tsconfig.json +1 -0
- package/dist/__mocks__/coloris.d.ts +0 -2
- package/dist/__mocks__/coloris.js +0 -5
@@ -11,22 +11,23 @@ interface PinchData {
|
|
11
11
|
dist: number;
|
12
12
|
}
|
13
13
|
export declare enum PanZoomMode {
|
14
|
-
|
15
|
-
|
16
|
-
|
14
|
+
OneFingerTouchGestures = 1,
|
15
|
+
TwoFingerTouchGestures = 2,
|
16
|
+
RightClickDrags = 4,
|
17
|
+
SinglePointerGestures = 8
|
17
18
|
}
|
18
19
|
export default class PanZoom extends BaseTool {
|
19
20
|
private editor;
|
20
21
|
private mode;
|
21
|
-
readonly kind: ToolType.PanZoom
|
22
|
+
readonly kind: ToolType.PanZoom;
|
22
23
|
private transform;
|
23
24
|
private lastAngle;
|
24
25
|
private lastDist;
|
25
26
|
private lastScreenCenter;
|
26
27
|
constructor(editor: Editor, mode: PanZoomMode, description: string);
|
27
28
|
computePinchData(p1: Pointer, p2: Pointer): PinchData;
|
28
|
-
private
|
29
|
-
onPointerDown({ allPointers }: PointerEvt): boolean;
|
29
|
+
private allPointersAreOfType;
|
30
|
+
onPointerDown({ allPointers: pointers }: PointerEvt): boolean;
|
30
31
|
private getCenterDelta;
|
31
32
|
private handleTwoFingerMove;
|
32
33
|
private handleOneFingerMove;
|
@@ -36,5 +37,7 @@ export default class PanZoom extends BaseTool {
|
|
36
37
|
private updateTransform;
|
37
38
|
onWheel({ delta, screenPos }: WheelEvt): boolean;
|
38
39
|
onKeyPress({ key }: KeyPressEvent): boolean;
|
40
|
+
setMode(mode: PanZoomMode): void;
|
41
|
+
getMode(): PanZoomMode;
|
39
42
|
}
|
40
43
|
export {};
|
@@ -2,17 +2,16 @@ import Mat33 from '../geometry/Mat33';
|
|
2
2
|
import { Vec2 } from '../geometry/Vec2';
|
3
3
|
import Vec3 from '../geometry/Vec3';
|
4
4
|
import { PointerDevice } from '../Pointer';
|
5
|
+
import { EditorEventType } from '../types';
|
5
6
|
import { Viewport } from '../Viewport';
|
6
7
|
import BaseTool from './BaseTool';
|
7
8
|
import { ToolType } from './ToolController';
|
8
9
|
export var PanZoomMode;
|
9
10
|
(function (PanZoomMode) {
|
10
|
-
|
11
|
-
PanZoomMode[PanZoomMode["
|
12
|
-
|
13
|
-
PanZoomMode[PanZoomMode["
|
14
|
-
// / Handle gestures from any device, rather than just touch
|
15
|
-
PanZoomMode[PanZoomMode["AnyDevice"] = 4] = "AnyDevice";
|
11
|
+
PanZoomMode[PanZoomMode["OneFingerTouchGestures"] = 1] = "OneFingerTouchGestures";
|
12
|
+
PanZoomMode[PanZoomMode["TwoFingerTouchGestures"] = 2] = "TwoFingerTouchGestures";
|
13
|
+
PanZoomMode[PanZoomMode["RightClickDrags"] = 4] = "RightClickDrags";
|
14
|
+
PanZoomMode[PanZoomMode["SinglePointerGestures"] = 8] = "SinglePointerGestures";
|
16
15
|
})(PanZoomMode || (PanZoomMode = {}));
|
17
16
|
export default class PanZoom extends BaseTool {
|
18
17
|
constructor(editor, mode, description) {
|
@@ -21,9 +20,6 @@ export default class PanZoom extends BaseTool {
|
|
21
20
|
this.mode = mode;
|
22
21
|
this.kind = ToolType.PanZoom;
|
23
22
|
this.transform = null;
|
24
|
-
if (mode === PanZoomMode.OneFingerGestures) {
|
25
|
-
this.kind = ToolType.TouchPanZoom;
|
26
|
-
}
|
27
23
|
}
|
28
24
|
// Returns information about the pointers in a gesture
|
29
25
|
computePinchData(p1, p2) {
|
@@ -34,24 +30,25 @@ export default class PanZoom extends BaseTool {
|
|
34
30
|
const screenCenter = p2.screenPos.plus(p1.screenPos).times(0.5);
|
35
31
|
return { canvasCenter, screenCenter, angle, dist };
|
36
32
|
}
|
37
|
-
|
38
|
-
return
|
33
|
+
allPointersAreOfType(pointers, kind) {
|
34
|
+
return pointers.every(pointer => pointer.device === kind);
|
39
35
|
}
|
40
|
-
onPointerDown({ allPointers }) {
|
36
|
+
onPointerDown({ allPointers: pointers }) {
|
41
37
|
var _a;
|
42
38
|
let handlingGesture = false;
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
const { screenCenter, angle, dist } = this.computePinchData(allPointers[0], allPointers[1]);
|
39
|
+
const allAreTouch = this.allPointersAreOfType(pointers, PointerDevice.Touch);
|
40
|
+
const isRightClick = this.allPointersAreOfType(pointers, PointerDevice.RightButtonMouse);
|
41
|
+
if (allAreTouch && pointers.length === 2 && this.mode & PanZoomMode.TwoFingerTouchGestures) {
|
42
|
+
const { screenCenter, angle, dist } = this.computePinchData(pointers[0], pointers[1]);
|
48
43
|
this.lastAngle = angle;
|
49
44
|
this.lastDist = dist;
|
50
45
|
this.lastScreenCenter = screenCenter;
|
51
46
|
handlingGesture = true;
|
52
47
|
}
|
53
|
-
else if (
|
54
|
-
this.
|
48
|
+
else if (pointers.length === 1 && ((this.mode & PanZoomMode.OneFingerTouchGestures && allAreTouch)
|
49
|
+
|| (isRightClick && this.mode & PanZoomMode.RightClickDrags)
|
50
|
+
|| (this.mode & PanZoomMode.SinglePointerGestures))) {
|
51
|
+
this.lastScreenCenter = pointers[0].screenPos;
|
55
52
|
handlingGesture = true;
|
56
53
|
}
|
57
54
|
if (handlingGesture) {
|
@@ -87,10 +84,10 @@ export default class PanZoom extends BaseTool {
|
|
87
84
|
var _a;
|
88
85
|
(_a = this.transform) !== null && _a !== void 0 ? _a : (this.transform = new Viewport.ViewportTransform(Mat33.identity));
|
89
86
|
const lastTransform = this.transform;
|
90
|
-
if (allPointers.length === 2
|
87
|
+
if (allPointers.length === 2) {
|
91
88
|
this.handleTwoFingerMove(allPointers);
|
92
89
|
}
|
93
|
-
else if (allPointers.length === 1
|
90
|
+
else if (allPointers.length === 1) {
|
94
91
|
this.handleOneFingerMove(allPointers[0]);
|
95
92
|
}
|
96
93
|
lastTransform.unapply(this.editor);
|
@@ -191,4 +188,16 @@ export default class PanZoom extends BaseTool {
|
|
191
188
|
this.updateTransform(transformUpdate);
|
192
189
|
return true;
|
193
190
|
}
|
191
|
+
setMode(mode) {
|
192
|
+
if (mode !== this.mode) {
|
193
|
+
this.mode = mode;
|
194
|
+
this.editor.notifier.dispatch(EditorEventType.ToolUpdated, {
|
195
|
+
kind: EditorEventType.ToolUpdated,
|
196
|
+
tool: this,
|
197
|
+
});
|
198
|
+
}
|
199
|
+
}
|
200
|
+
getMode() {
|
201
|
+
return this.mode;
|
202
|
+
}
|
194
203
|
}
|
package/dist/src/tools/Pen.js
CHANGED
@@ -66,9 +66,14 @@ export default class Pen extends BaseTool {
|
|
66
66
|
if (this.builder && current.isPrimary) {
|
67
67
|
const stroke = this.builder.build();
|
68
68
|
this.previewStroke();
|
69
|
-
|
70
|
-
|
71
|
-
|
69
|
+
if (stroke.getBBox().area > 0) {
|
70
|
+
const canFlatten = true;
|
71
|
+
const action = new EditorImage.AddElementCommand(stroke, canFlatten);
|
72
|
+
this.editor.dispatch(action);
|
73
|
+
}
|
74
|
+
else {
|
75
|
+
console.warn('Pen: Not adding empty stroke', stroke, 'to the canvas.');
|
76
|
+
}
|
72
77
|
}
|
73
78
|
this.builder = null;
|
74
79
|
this.editor.clearWetInk();
|
@@ -13,7 +13,6 @@ import Mat33 from '../geometry/Mat33';
|
|
13
13
|
import Rect2 from '../geometry/Rect2';
|
14
14
|
import { Vec2 } from '../geometry/Vec2';
|
15
15
|
import { EditorEventType } from '../types';
|
16
|
-
import Viewport from '../Viewport';
|
17
16
|
import BaseTool from './BaseTool';
|
18
17
|
import { ToolType } from './ToolController';
|
19
18
|
const handleScreenSize = 30;
|
@@ -115,7 +114,7 @@ const makeDraggable = (element, onDrag, onDragEnd) => {
|
|
115
114
|
element.addEventListener('pointercancel', onPointerEnd);
|
116
115
|
};
|
117
116
|
// Maximum number of strokes to transform without a re-render.
|
118
|
-
const updateChunkSize =
|
117
|
+
const updateChunkSize = 100;
|
119
118
|
class Selection {
|
120
119
|
constructor(startPoint, editor) {
|
121
120
|
this.startPoint = startPoint;
|
@@ -285,10 +284,13 @@ class Selection {
|
|
285
284
|
if (this.region.containsRect(elem.getBBox())) {
|
286
285
|
return true;
|
287
286
|
}
|
288
|
-
|
289
|
-
|
287
|
+
// Calculated bounding boxes can be slightly larger than their actual contents' bounding box.
|
288
|
+
// As such, test with more lines than just this' edges.
|
289
|
+
const testLines = [];
|
290
|
+
for (const subregion of this.region.divideIntoGrid(2, 2)) {
|
291
|
+
testLines.push(...subregion.getEdges());
|
290
292
|
}
|
291
|
-
return
|
293
|
+
return testLines.some(edge => elem.intersects(edge));
|
292
294
|
});
|
293
295
|
// Find the bounding box of all selected elements.
|
294
296
|
if (!this.recomputeRegion()) {
|
@@ -392,26 +394,9 @@ export default class SelectionTool extends BaseTool {
|
|
392
394
|
tool: this,
|
393
395
|
});
|
394
396
|
if (hasSelection) {
|
395
|
-
const visibleRect = this.editor.viewport.visibleRect;
|
396
|
-
const selectionRect = this.selectionBox.region;
|
397
397
|
this.editor.announceForAccessibility(this.editor.localization.selectedElements(this.selectionBox.getSelectedItemCount()));
|
398
|
-
|
399
|
-
|
400
|
-
// Ensure that the selection fits within the target
|
401
|
-
if (targetRect.w < selectionRect.w || targetRect.h < selectionRect.h) {
|
402
|
-
const multiplier = Math.max(selectionRect.w / targetRect.w, selectionRect.h / targetRect.h);
|
403
|
-
const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
|
404
|
-
const viewportContentTransform = visibleRectTransform.inverse();
|
405
|
-
(new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
|
406
|
-
}
|
407
|
-
// Ensure that the top left is visible
|
408
|
-
if (!targetRect.containsRect(selectionRect)) {
|
409
|
-
// target position - current position
|
410
|
-
const translation = selectionRect.center.minus(targetRect.center);
|
411
|
-
const visibleRectTransform = Mat33.translation(translation);
|
412
|
-
const viewportContentTransform = visibleRectTransform.inverse();
|
413
|
-
(new Viewport.ViewportTransform(viewportContentTransform)).apply(this.editor);
|
414
|
-
}
|
398
|
+
const selectionRect = this.selectionBox.region;
|
399
|
+
this.editor.viewport.zoomTo(selectionRect).apply(this.editor);
|
415
400
|
}
|
416
401
|
}
|
417
402
|
onPointerUp(event) {
|
@@ -3,12 +3,11 @@ import Editor from '../Editor';
|
|
3
3
|
import BaseTool from './BaseTool';
|
4
4
|
import { ToolLocalization } from './localization';
|
5
5
|
export declare enum ToolType {
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
UndoRedoShortcut = 5
|
6
|
+
Pen = 0,
|
7
|
+
Selection = 1,
|
8
|
+
Eraser = 2,
|
9
|
+
PanZoom = 3,
|
10
|
+
UndoRedoShortcut = 4
|
12
11
|
}
|
13
12
|
export default class ToolController {
|
14
13
|
private tools;
|
@@ -8,17 +8,16 @@ import Color4 from '../Color4';
|
|
8
8
|
import UndoRedoShortcut from './UndoRedoShortcut';
|
9
9
|
export var ToolType;
|
10
10
|
(function (ToolType) {
|
11
|
-
ToolType[ToolType["
|
12
|
-
ToolType[ToolType["
|
13
|
-
ToolType[ToolType["
|
14
|
-
ToolType[ToolType["
|
15
|
-
ToolType[ToolType["
|
16
|
-
ToolType[ToolType["UndoRedoShortcut"] = 5] = "UndoRedoShortcut";
|
11
|
+
ToolType[ToolType["Pen"] = 0] = "Pen";
|
12
|
+
ToolType[ToolType["Selection"] = 1] = "Selection";
|
13
|
+
ToolType[ToolType["Eraser"] = 2] = "Eraser";
|
14
|
+
ToolType[ToolType["PanZoom"] = 3] = "PanZoom";
|
15
|
+
ToolType[ToolType["UndoRedoShortcut"] = 4] = "UndoRedoShortcut";
|
17
16
|
})(ToolType || (ToolType = {}));
|
18
17
|
export default class ToolController {
|
19
18
|
constructor(editor, localization) {
|
20
19
|
const primaryToolEnabledGroup = new ToolEnabledGroup();
|
21
|
-
const
|
20
|
+
const panZoomTool = new PanZoom(editor, PanZoomMode.TwoFingerTouchGestures | PanZoomMode.RightClickDrags, localization.touchPanTool);
|
22
21
|
const primaryPenTool = new Pen(editor, localization.penTool(1), { color: Color4.purple, thickness: 16 });
|
23
22
|
const primaryTools = [
|
24
23
|
new SelectionTool(editor, localization.selectionTool),
|
@@ -30,13 +29,12 @@ export default class ToolController {
|
|
30
29
|
new Pen(editor, localization.penTool(3), { color: Color4.ofRGBA(1, 1, 0, 0.5), thickness: 64 }),
|
31
30
|
];
|
32
31
|
this.tools = [
|
33
|
-
|
32
|
+
panZoomTool,
|
34
33
|
...primaryTools,
|
35
|
-
new PanZoom(editor, PanZoomMode.TwoFingerGestures | PanZoomMode.AnyDevice, localization.twoFingerPanZoomTool),
|
36
34
|
new UndoRedoShortcut(editor),
|
37
35
|
];
|
38
36
|
primaryTools.forEach(tool => tool.setToolGroup(primaryToolEnabledGroup));
|
39
|
-
|
37
|
+
panZoomTool.setEnabled(true);
|
40
38
|
primaryPenTool.setEnabled(true);
|
41
39
|
editor.notifier.on(EditorEventType.ToolEnabled, event => {
|
42
40
|
if (event.kind === EditorEventType.ToolEnabled) {
|
@@ -5,6 +5,7 @@ export const defaultToolLocalization = {
|
|
5
5
|
touchPanTool: 'Touch Panning',
|
6
6
|
twoFingerPanZoomTool: 'Panning and Zooming',
|
7
7
|
undoRedoTool: 'Undo/Redo',
|
8
|
+
RightClickDragPanTool: 'Right-click drag',
|
8
9
|
toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
|
9
10
|
toolDisabledAnnouncement: (toolName) => `${toolName} disabled`,
|
10
11
|
};
|
package/dist/src/types.d.ts
CHANGED
@@ -89,8 +89,9 @@ export interface ColorPickerToggled {
|
|
89
89
|
export declare type EditorEventDataType = EditorToolEvent | EditorObjectEvent | EditorViewportChangedEvent | DisplayResizedEvent | EditorUndoStackUpdated | ColorPickerToggled;
|
90
90
|
export declare type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null;
|
91
91
|
export declare type ComponentAddedListener = (component: AbstractComponent) => void;
|
92
|
+
export declare type OnDetermineExportRectListener = (exportRect: Rect2) => void;
|
92
93
|
export interface ImageLoader {
|
93
|
-
start(onAddComponent: ComponentAddedListener, onProgressListener: OnProgressListener): Promise<
|
94
|
+
start(onAddComponent: ComponentAddedListener, onProgressListener: OnProgressListener, onDetermineExportRect?: OnDetermineExportRectListener): Promise<void>;
|
94
95
|
}
|
95
96
|
export interface StrokeDataPoint {
|
96
97
|
pos: Point2;
|
package/package.json
CHANGED
package/src/Editor.ts
CHANGED
@@ -9,9 +9,9 @@ import EventDispatcher from './EventDispatcher';
|
|
9
9
|
import { Point2, Vec2 } from './geometry/Vec2';
|
10
10
|
import Vec3 from './geometry/Vec3';
|
11
11
|
import HTMLToolbar from './toolbar/HTMLToolbar';
|
12
|
-
import { RenderablePathSpec } from './rendering/AbstractRenderer';
|
13
|
-
import Display, { RenderingMode } from './Display';
|
14
|
-
import SVGRenderer from './rendering/SVGRenderer';
|
12
|
+
import { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
|
13
|
+
import Display, { RenderingMode } from './rendering/Display';
|
14
|
+
import SVGRenderer from './rendering/renderers/SVGRenderer';
|
15
15
|
import Color4 from './Color4';
|
16
16
|
import SVGLoader from './SVGLoader';
|
17
17
|
import Pointer from './Pointer';
|
@@ -165,6 +165,10 @@ export class Editor {
|
|
165
165
|
// May be required to prevent text selection on iOS/Safari:
|
166
166
|
// See https://stackoverflow.com/a/70992717/17055750
|
167
167
|
this.renderingRegion.addEventListener('touchstart', evt => evt.preventDefault());
|
168
|
+
this.renderingRegion.addEventListener('contextmenu', evt => {
|
169
|
+
// Don't show a context menu
|
170
|
+
evt.preventDefault();
|
171
|
+
});
|
168
172
|
|
169
173
|
this.renderingRegion.addEventListener('pointerdown', evt => {
|
170
174
|
const pointer = Pointer.ofEvent(evt, true, this.viewport);
|
@@ -380,7 +384,8 @@ export class Editor {
|
|
380
384
|
);
|
381
385
|
}
|
382
386
|
|
383
|
-
this.image.render(renderer, this.viewport);
|
387
|
+
//this.image.render(renderer, this.viewport);
|
388
|
+
this.image.renderWithCache(renderer, this.display.getCache(), this.viewport);
|
384
389
|
this.rerenderQueued = false;
|
385
390
|
}
|
386
391
|
|
@@ -463,22 +468,28 @@ export class Editor {
|
|
463
468
|
|
464
469
|
public async loadFrom(loader: ImageLoader) {
|
465
470
|
this.showLoadingWarning(0);
|
466
|
-
|
471
|
+
this.display.setDraftMode(true);
|
472
|
+
|
473
|
+
await loader.start((component) => {
|
467
474
|
(new EditorImage.AddElementCommand(component)).apply(this);
|
468
475
|
}, (countProcessed: number, totalToProcess: number) => {
|
469
|
-
if (countProcessed %
|
476
|
+
if (countProcessed % 500 === 0) {
|
470
477
|
this.showLoadingWarning(countProcessed / totalToProcess);
|
471
|
-
this.rerender(
|
478
|
+
this.rerender();
|
472
479
|
return new Promise(resolve => {
|
473
480
|
requestAnimationFrame(() => resolve());
|
474
481
|
});
|
475
482
|
}
|
476
483
|
|
477
484
|
return null;
|
485
|
+
}, (importExportRect: Rect2) => {
|
486
|
+
this.setImportExportRect(importExportRect).apply(this);
|
487
|
+
this.viewport.zoomTo(importExportRect).apply(this);
|
478
488
|
});
|
479
489
|
this.hideLoadingWarning();
|
480
490
|
|
481
|
-
this.
|
491
|
+
this.display.setDraftMode(false);
|
492
|
+
this.queueRerender();
|
482
493
|
}
|
483
494
|
|
484
495
|
// Returns the size of the visible region of the output SVG
|
package/src/EditorImage.test.ts
CHANGED
@@ -5,8 +5,8 @@ import Stroke from './components/Stroke';
|
|
5
5
|
import { Vec2 } from './geometry/Vec2';
|
6
6
|
import Path, { PathCommandType } from './geometry/Path';
|
7
7
|
import Color4 from './Color4';
|
8
|
-
import DummyRenderer from './rendering/DummyRenderer';
|
9
|
-
import { RenderingStyle } from './rendering/AbstractRenderer';
|
8
|
+
import DummyRenderer from './rendering/renderers/DummyRenderer';
|
9
|
+
import { RenderingStyle } from './rendering/renderers/AbstractRenderer';
|
10
10
|
import createEditor from './testing/createEditor';
|
11
11
|
|
12
12
|
describe('EditorImage', () => {
|
package/src/EditorImage.ts
CHANGED
@@ -1,10 +1,15 @@
|
|
1
1
|
import Editor from './Editor';
|
2
|
-
import AbstractRenderer from './rendering/AbstractRenderer';
|
2
|
+
import AbstractRenderer from './rendering/renderers/AbstractRenderer';
|
3
3
|
import Command from './commands/Command';
|
4
4
|
import Viewport from './Viewport';
|
5
5
|
import AbstractComponent from './components/AbstractComponent';
|
6
6
|
import Rect2 from './geometry/Rect2';
|
7
7
|
import { EditorLocalization } from './localization';
|
8
|
+
import RenderingCache from './rendering/caching/RenderingCache';
|
9
|
+
|
10
|
+
export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
|
11
|
+
leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());
|
12
|
+
};
|
8
13
|
|
9
14
|
// Handles lookup/storage of elements in the image
|
10
15
|
export default class EditorImage {
|
@@ -20,7 +25,7 @@ export default class EditorImage {
|
|
20
25
|
|
21
26
|
// Returns the parent of the given element, if it exists.
|
22
27
|
public findParent(elem: AbstractComponent): ImageNode|null {
|
23
|
-
const candidates = this.root.
|
28
|
+
const candidates = this.root.getLeavesIntersectingRegion(elem.getBBox());
|
24
29
|
for (const candidate of candidates) {
|
25
30
|
if (candidate.getContent() === elem) {
|
26
31
|
return candidate;
|
@@ -29,25 +34,18 @@ export default class EditorImage {
|
|
29
34
|
return null;
|
30
35
|
}
|
31
36
|
|
32
|
-
|
33
|
-
|
37
|
+
public renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport) {
|
38
|
+
cache.render(screenRenderer, this.root, viewport);
|
34
39
|
}
|
35
40
|
|
36
|
-
public render(renderer: AbstractRenderer, viewport: Viewport
|
37
|
-
|
38
|
-
const leaves = this.root.getLeavesInRegion(viewport.visibleRect, minFraction);
|
39
|
-
this.sortLeaves(leaves);
|
40
|
-
|
41
|
-
for (const leaf of leaves) {
|
42
|
-
// Leaves by definition have content
|
43
|
-
leaf.getContent()!.render(renderer, viewport.visibleRect);
|
44
|
-
}
|
41
|
+
public render(renderer: AbstractRenderer, viewport: Viewport) {
|
42
|
+
this.root.render(renderer, viewport.visibleRect);
|
45
43
|
}
|
46
44
|
|
47
45
|
// Renders all nodes, even ones not within the viewport
|
48
46
|
public renderAll(renderer: AbstractRenderer) {
|
49
47
|
const leaves = this.root.getLeaves();
|
50
|
-
|
48
|
+
sortLeavesByZIndex(leaves);
|
51
49
|
|
52
50
|
for (const leaf of leaves) {
|
53
51
|
leaf.getContent()!.render(renderer, leaf.getBBox());
|
@@ -55,8 +53,9 @@ export default class EditorImage {
|
|
55
53
|
}
|
56
54
|
|
57
55
|
public getElementsIntersectingRegion(region: Rect2): AbstractComponent[] {
|
58
|
-
const leaves = this.root.
|
59
|
-
|
56
|
+
const leaves = this.root.getLeavesIntersectingRegion(region);
|
57
|
+
sortLeavesByZIndex(leaves);
|
58
|
+
|
60
59
|
return leaves.map(leaf => leaf.getContent()!);
|
61
60
|
}
|
62
61
|
|
@@ -74,6 +73,10 @@ export default class EditorImage {
|
|
74
73
|
) {
|
75
74
|
this.#element = element;
|
76
75
|
this.#applyByFlattening = applyByFlattening;
|
76
|
+
|
77
|
+
if (isNaN(this.#element.getBBox().area)) {
|
78
|
+
throw new Error('Elements in the image cannot have NaN bounding boxes');
|
79
|
+
}
|
77
80
|
}
|
78
81
|
|
79
82
|
public apply(editor: Editor) {
|
@@ -100,15 +103,17 @@ export default class EditorImage {
|
|
100
103
|
}
|
101
104
|
|
102
105
|
export type AddElementCommand = typeof EditorImage.AddElementCommand.prototype;
|
106
|
+
type TooSmallToRenderCheck = (rect: Rect2)=> boolean;
|
103
107
|
|
104
|
-
|
108
|
+
// TODO: Assign leaf nodes to CacheNodes. When leaf nodes are modified, the corresponding CacheNodes can be updated.
|
105
109
|
export class ImageNode {
|
106
110
|
private content: AbstractComponent|null;
|
107
111
|
private bbox: Rect2;
|
108
112
|
private children: ImageNode[];
|
109
113
|
private targetChildCount: number = 30;
|
110
|
-
|
111
|
-
private
|
114
|
+
|
115
|
+
private id: number;
|
116
|
+
private static idCounter: number = 0;
|
112
117
|
|
113
118
|
public constructor(
|
114
119
|
private parent: ImageNode|null = null
|
@@ -117,8 +122,15 @@ export class ImageNode {
|
|
117
122
|
this.bbox = Rect2.empty;
|
118
123
|
this.content = null;
|
119
124
|
|
120
|
-
this.
|
121
|
-
|
125
|
+
this.id = ImageNode.idCounter++;
|
126
|
+
}
|
127
|
+
|
128
|
+
public getId() {
|
129
|
+
return this.id;
|
130
|
+
}
|
131
|
+
|
132
|
+
public onContentChange() {
|
133
|
+
this.id = ImageNode.idCounter++;
|
122
134
|
}
|
123
135
|
|
124
136
|
public getContent(): AbstractComponent|null {
|
@@ -129,18 +141,25 @@ export class ImageNode {
|
|
129
141
|
return this.parent;
|
130
142
|
}
|
131
143
|
|
132
|
-
private
|
144
|
+
private getChildrenIntersectingRegion(region: Rect2): ImageNode[] {
|
133
145
|
return this.children.filter(child => {
|
134
146
|
return child.getBBox().intersects(region);
|
135
147
|
});
|
136
148
|
}
|
137
149
|
|
150
|
+
public getChildrenOrSelfIntersectingRegion(region: Rect2): ImageNode[] {
|
151
|
+
if (this.content) {
|
152
|
+
return [this];
|
153
|
+
}
|
154
|
+
return this.getChildrenIntersectingRegion(region);
|
155
|
+
}
|
156
|
+
|
138
157
|
// Returns a list of `ImageNode`s with content (and thus no children).
|
139
|
-
public
|
158
|
+
public getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[] {
|
140
159
|
const result: ImageNode[] = [];
|
141
160
|
|
142
161
|
// Don't render if too small
|
143
|
-
if (this.bbox
|
162
|
+
if (isTooSmall?.(this.bbox)) {
|
144
163
|
return [];
|
145
164
|
}
|
146
165
|
|
@@ -148,9 +167,9 @@ export class ImageNode {
|
|
148
167
|
result.push(this);
|
149
168
|
}
|
150
169
|
|
151
|
-
const children = this.
|
170
|
+
const children = this.getChildrenIntersectingRegion(region);
|
152
171
|
for (const child of children) {
|
153
|
-
result.push(...child.
|
172
|
+
result.push(...child.getLeavesIntersectingRegion(region, isTooSmall));
|
154
173
|
}
|
155
174
|
|
156
175
|
return result;
|
@@ -172,6 +191,8 @@ export class ImageNode {
|
|
172
191
|
}
|
173
192
|
|
174
193
|
public addLeaf(leaf: AbstractComponent): ImageNode {
|
194
|
+
this.onContentChange();
|
195
|
+
|
175
196
|
if (this.content === null && this.children.length === 0) {
|
176
197
|
this.content = leaf;
|
177
198
|
this.recomputeBBox(true);
|
@@ -239,12 +260,8 @@ export class ImageNode {
|
|
239
260
|
const oldBBox = this.bbox;
|
240
261
|
if (this.content !== null) {
|
241
262
|
this.bbox = this.content.getBBox();
|
242
|
-
this.minZIndex = this.content.zIndex;
|
243
|
-
this.maxZIndex = this.content.zIndex;
|
244
263
|
} else {
|
245
264
|
this.bbox = Rect2.empty;
|
246
|
-
this.minZIndex = null;
|
247
|
-
this.maxZIndex = null;
|
248
265
|
let isFirst = true;
|
249
266
|
|
250
267
|
for (const child of this.children) {
|
@@ -254,15 +271,6 @@ export class ImageNode {
|
|
254
271
|
} else {
|
255
272
|
this.bbox = this.bbox.union(child.getBBox());
|
256
273
|
}
|
257
|
-
|
258
|
-
this.minZIndex ??= child.minZIndex;
|
259
|
-
this.maxZIndex ??= child.maxZIndex;
|
260
|
-
if (child.minZIndex !== null && this.minZIndex !== null) {
|
261
|
-
this.minZIndex = Math.min(child.minZIndex, this.minZIndex);
|
262
|
-
}
|
263
|
-
if (child.maxZIndex !== null && this.maxZIndex !== null) {
|
264
|
-
this.maxZIndex = Math.max(child.maxZIndex, this.maxZIndex);
|
265
|
-
}
|
266
274
|
}
|
267
275
|
}
|
268
276
|
|
@@ -295,9 +303,6 @@ export class ImageNode {
|
|
295
303
|
|
296
304
|
// Remove this node and all of its children
|
297
305
|
public remove() {
|
298
|
-
this.minZIndex = null;
|
299
|
-
this.maxZIndex = null;
|
300
|
-
|
301
306
|
if (!this.parent) {
|
302
307
|
this.content = null;
|
303
308
|
this.children = [];
|
@@ -322,4 +327,15 @@ export class ImageNode {
|
|
322
327
|
this.parent = null;
|
323
328
|
this.children = [];
|
324
329
|
}
|
330
|
+
|
331
|
+
public render(renderer: AbstractRenderer, visibleRect: Rect2) {
|
332
|
+
// Don't render components that are < 0.1% of the viewport.
|
333
|
+
const leaves = this.getLeavesIntersectingRegion(visibleRect, rect => renderer.isTooSmallToRender(rect));
|
334
|
+
sortLeavesByZIndex(leaves);
|
335
|
+
|
336
|
+
for (const leaf of leaves) {
|
337
|
+
// Leaves by definition have content
|
338
|
+
leaf.getContent()!.render(renderer, visibleRect);
|
339
|
+
}
|
340
|
+
}
|
325
341
|
}
|
package/src/Pointer.ts
CHANGED
@@ -5,7 +5,8 @@ export enum PointerDevice {
|
|
5
5
|
Pen,
|
6
6
|
Eraser,
|
7
7
|
Touch,
|
8
|
-
|
8
|
+
PrimaryButtonMouse,
|
9
|
+
RightButtonMouse,
|
9
10
|
Other,
|
10
11
|
}
|
11
12
|
|
@@ -31,7 +32,7 @@ export default class Pointer {
|
|
31
32
|
public readonly id: number,
|
32
33
|
|
33
34
|
// Numeric timestamp (milliseconds, as from (new Date).getTime())
|
34
|
-
public readonly timeStamp: number
|
35
|
+
public readonly timeStamp: number,
|
35
36
|
) {
|
36
37
|
}
|
37
38
|
|
@@ -39,7 +40,7 @@ export default class Pointer {
|
|
39
40
|
const screenPos = Vec2.of(evt.offsetX, evt.offsetY);
|
40
41
|
|
41
42
|
const pointerTypeToDevice: Record<string, PointerDevice> = {
|
42
|
-
'mouse': PointerDevice.
|
43
|
+
'mouse': PointerDevice.PrimaryButtonMouse,
|
43
44
|
'pen': PointerDevice.Pen,
|
44
45
|
'touch': PointerDevice.Touch,
|
45
46
|
};
|
@@ -53,6 +54,14 @@ export default class Pointer {
|
|
53
54
|
const timeStamp = (new Date()).getTime();
|
54
55
|
const canvasPos = viewport.roundPoint(viewport.screenToCanvas(screenPos));
|
55
56
|
|
57
|
+
if (device === PointerDevice.PrimaryButtonMouse) {
|
58
|
+
if (evt.buttons & 0x2) {
|
59
|
+
device = PointerDevice.RightButtonMouse;
|
60
|
+
} else if (!(evt.buttons & 0x1)) {
|
61
|
+
device = PointerDevice.Other;
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
56
65
|
return new Pointer(
|
57
66
|
screenPos,
|
58
67
|
canvasPos,
|
@@ -61,7 +70,7 @@ export default class Pointer {
|
|
61
70
|
isDown,
|
62
71
|
device,
|
63
72
|
evt.pointerId,
|
64
|
-
timeStamp
|
73
|
+
timeStamp,
|
65
74
|
);
|
66
75
|
}
|
67
76
|
|