js-draw 0.1.4 → 0.1.7
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/.eslintrc.js +1 -0
- package/CHANGELOG.md +15 -0
- package/README.md +2 -2
- package/dist/bundle.js +1 -1
- package/dist/src/Color4.js +6 -2
- package/dist/src/Editor.d.ts +1 -0
- package/dist/src/Editor.js +20 -9
- package/dist/src/EditorImage.d.ts +8 -13
- package/dist/src/EditorImage.js +51 -29
- package/dist/src/SVGLoader.js +6 -2
- package/dist/src/Viewport.d.ts +10 -2
- package/dist/src/Viewport.js +8 -6
- package/dist/src/commands/Command.d.ts +9 -8
- package/dist/src/commands/Command.js +15 -14
- package/dist/src/commands/Duplicate.d.ts +14 -0
- package/dist/src/commands/Duplicate.js +34 -0
- package/dist/src/commands/Erase.d.ts +5 -2
- package/dist/src/commands/Erase.js +28 -9
- package/dist/src/commands/SerializableCommand.d.ts +13 -0
- package/dist/src/commands/SerializableCommand.js +28 -0
- package/dist/src/commands/localization.d.ts +2 -0
- package/dist/src/commands/localization.js +2 -0
- package/dist/src/components/AbstractComponent.d.ts +15 -2
- package/dist/src/components/AbstractComponent.js +122 -26
- package/dist/src/components/SVGGlobalAttributesObject.d.ts +6 -1
- package/dist/src/components/SVGGlobalAttributesObject.js +23 -1
- package/dist/src/components/Stroke.d.ts +5 -0
- package/dist/src/components/Stroke.js +32 -1
- package/dist/src/components/Text.d.ts +11 -4
- package/dist/src/components/Text.js +57 -3
- package/dist/src/components/UnknownSVGObject.d.ts +2 -0
- package/dist/src/components/UnknownSVGObject.js +12 -1
- package/dist/src/components/util/describeComponentList.d.ts +4 -0
- package/dist/src/components/util/describeComponentList.js +14 -0
- package/dist/src/geometry/Path.d.ts +4 -1
- package/dist/src/geometry/Path.js +4 -0
- package/dist/src/localization.d.ts +2 -1
- package/dist/src/localization.js +2 -1
- package/dist/src/rendering/Display.d.ts +5 -0
- package/dist/src/rendering/Display.js +32 -0
- package/dist/src/rendering/RenderingStyle.d.ts +24 -0
- package/dist/src/rendering/RenderingStyle.js +32 -0
- package/dist/src/rendering/localization.d.ts +5 -0
- package/dist/src/rendering/localization.js +4 -0
- package/dist/src/rendering/renderers/AbstractRenderer.d.ts +1 -8
- package/dist/src/rendering/renderers/AbstractRenderer.js +1 -6
- package/dist/src/rendering/renderers/CanvasRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/DummyRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/SVGRenderer.d.ts +2 -1
- package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +25 -0
- package/dist/src/rendering/renderers/TextOnlyRenderer.js +40 -0
- package/dist/src/toolbar/HTMLToolbar.d.ts +1 -1
- package/dist/src/toolbar/HTMLToolbar.js +52 -534
- package/dist/src/toolbar/icons.d.ts +5 -0
- package/dist/src/toolbar/icons.js +186 -13
- package/dist/src/toolbar/localization.d.ts +4 -0
- package/dist/src/toolbar/localization.js +4 -0
- package/dist/src/toolbar/makeColorInput.d.ts +5 -0
- package/dist/src/toolbar/makeColorInput.js +81 -0
- package/dist/src/toolbar/widgets/BaseToolWidget.d.ts +12 -0
- package/dist/src/toolbar/widgets/BaseToolWidget.js +44 -0
- package/dist/src/toolbar/widgets/BaseWidget.d.ts +32 -0
- package/dist/src/toolbar/widgets/BaseWidget.js +148 -0
- package/dist/src/toolbar/widgets/EraserWidget.d.ts +6 -0
- package/dist/src/toolbar/widgets/EraserWidget.js +14 -0
- package/dist/src/toolbar/widgets/HandToolWidget.d.ts +13 -0
- package/dist/src/toolbar/widgets/HandToolWidget.js +133 -0
- package/dist/src/toolbar/widgets/PenWidget.d.ts +20 -0
- package/dist/src/toolbar/widgets/PenWidget.js +131 -0
- package/dist/src/toolbar/widgets/SelectionWidget.d.ts +11 -0
- package/dist/src/toolbar/widgets/SelectionWidget.js +56 -0
- package/dist/src/toolbar/widgets/TextToolWidget.d.ts +13 -0
- package/dist/src/toolbar/widgets/TextToolWidget.js +72 -0
- package/dist/src/tools/Pen.js +1 -1
- package/dist/src/tools/PipetteTool.d.ts +20 -0
- package/dist/src/tools/PipetteTool.js +40 -0
- package/dist/src/tools/SelectionTool.d.ts +2 -0
- package/dist/src/tools/SelectionTool.js +41 -23
- package/dist/src/tools/TextTool.d.ts +1 -0
- package/dist/src/tools/TextTool.js +5 -4
- package/dist/src/tools/ToolController.d.ts +3 -1
- package/dist/src/tools/ToolController.js +4 -0
- package/dist/src/tools/localization.d.ts +2 -1
- package/dist/src/tools/localization.js +3 -2
- package/dist/src/types.d.ts +7 -2
- package/dist/src/types.js +1 -0
- package/jest.config.js +2 -0
- package/package.json +6 -6
- package/src/Color4.ts +9 -3
- package/src/Editor.css +10 -0
- package/src/Editor.ts +24 -12
- package/src/EditorImage.test.ts +4 -4
- package/src/EditorImage.ts +61 -20
- package/src/SVGLoader.ts +9 -3
- package/src/Viewport.ts +7 -6
- package/src/commands/Command.ts +21 -19
- package/src/commands/Duplicate.ts +49 -0
- package/src/commands/Erase.ts +34 -13
- package/src/commands/SerializableCommand.ts +41 -0
- package/src/commands/localization.ts +5 -0
- package/src/components/AbstractComponent.ts +168 -26
- package/src/components/SVGGlobalAttributesObject.ts +34 -2
- package/src/components/Stroke.test.ts +53 -0
- package/src/components/Stroke.ts +37 -2
- package/src/components/Text.test.ts +38 -0
- package/src/components/Text.ts +80 -5
- package/src/components/UnknownSVGObject.test.ts +10 -0
- package/src/components/UnknownSVGObject.ts +15 -1
- package/src/components/builders/FreehandLineBuilder.ts +2 -1
- package/src/components/util/describeComponentList.ts +18 -0
- package/src/geometry/Path.ts +8 -1
- package/src/localization.ts +3 -1
- package/src/rendering/Display.ts +43 -1
- package/src/rendering/RenderingStyle.test.ts +68 -0
- package/src/rendering/RenderingStyle.ts +46 -0
- package/src/rendering/caching/RenderingCache.test.ts +1 -1
- package/src/rendering/localization.ts +10 -0
- package/src/rendering/renderers/AbstractRenderer.ts +1 -15
- package/src/rendering/renderers/CanvasRenderer.ts +2 -1
- package/src/rendering/renderers/DummyRenderer.ts +2 -1
- package/src/rendering/renderers/SVGRenderer.ts +2 -1
- package/src/rendering/renderers/TextOnlyRenderer.ts +52 -0
- package/src/toolbar/HTMLToolbar.ts +58 -660
- package/src/toolbar/icons.ts +205 -13
- package/src/toolbar/localization.ts +10 -2
- package/src/toolbar/makeColorInput.ts +105 -0
- package/src/toolbar/toolbar.css +116 -78
- package/src/toolbar/widgets/BaseToolWidget.ts +53 -0
- package/src/toolbar/widgets/BaseWidget.ts +175 -0
- package/src/toolbar/widgets/EraserWidget.ts +16 -0
- package/src/toolbar/widgets/HandToolWidget.ts +186 -0
- package/src/toolbar/widgets/PenWidget.ts +165 -0
- package/src/toolbar/widgets/SelectionWidget.ts +72 -0
- package/src/toolbar/widgets/TextToolWidget.ts +90 -0
- package/src/tools/Pen.ts +1 -1
- package/src/tools/PipetteTool.ts +56 -0
- package/src/tools/SelectionTool.test.ts +2 -4
- package/src/tools/SelectionTool.ts +47 -27
- package/src/tools/TextTool.ts +7 -3
- package/src/tools/ToolController.ts +10 -6
- package/src/tools/UndoRedoShortcut.test.ts +1 -1
- package/src/tools/localization.ts +6 -3
- package/src/types.ts +12 -1
@@ -7,6 +7,7 @@ import SelectionTool from './SelectionTool';
|
|
7
7
|
import Color4 from '../Color4';
|
8
8
|
import UndoRedoShortcut from './UndoRedoShortcut';
|
9
9
|
import TextTool from './TextTool';
|
10
|
+
import PipetteTool from './PipetteTool';
|
10
11
|
export var ToolType;
|
11
12
|
(function (ToolType) {
|
12
13
|
ToolType[ToolType["Pen"] = 0] = "Pen";
|
@@ -15,6 +16,8 @@ export var ToolType;
|
|
15
16
|
ToolType[ToolType["PanZoom"] = 3] = "PanZoom";
|
16
17
|
ToolType[ToolType["Text"] = 4] = "Text";
|
17
18
|
ToolType[ToolType["UndoRedoShortcut"] = 5] = "UndoRedoShortcut";
|
19
|
+
ToolType[ToolType["Pipette"] = 6] = "Pipette";
|
20
|
+
ToolType[ToolType["Other"] = 7] = "Other";
|
18
21
|
})(ToolType || (ToolType = {}));
|
19
22
|
export default class ToolController {
|
20
23
|
constructor(editor, localization) {
|
@@ -32,6 +35,7 @@ export default class ToolController {
|
|
32
35
|
new TextTool(editor, localization.textTool, localization),
|
33
36
|
];
|
34
37
|
this.tools = [
|
38
|
+
new PipetteTool(editor, localization.pipetteTool),
|
35
39
|
panZoomTool,
|
36
40
|
...primaryTools,
|
37
41
|
new UndoRedoShortcut(editor),
|
@@ -1,11 +1,12 @@
|
|
1
1
|
export interface ToolLocalization {
|
2
|
-
rightClickDragPanTool: string;
|
3
2
|
penTool: (penId: number) => string;
|
4
3
|
selectionTool: string;
|
5
4
|
eraserTool: string;
|
6
5
|
touchPanTool: string;
|
7
6
|
twoFingerPanZoomTool: string;
|
8
7
|
undoRedoTool: string;
|
8
|
+
pipetteTool: string;
|
9
|
+
rightClickDragPanTool: string;
|
9
10
|
textTool: string;
|
10
11
|
enterTextToInsert: string;
|
11
12
|
toolEnabledAnnouncement: (toolName: string) => string;
|
@@ -2,10 +2,11 @@ export const defaultToolLocalization = {
|
|
2
2
|
penTool: (penId) => `Pen ${penId}`,
|
3
3
|
selectionTool: 'Selection',
|
4
4
|
eraserTool: 'Eraser',
|
5
|
-
touchPanTool: 'Touch
|
6
|
-
twoFingerPanZoomTool: 'Panning and
|
5
|
+
touchPanTool: 'Touch panning',
|
6
|
+
twoFingerPanZoomTool: 'Panning and zooming',
|
7
7
|
undoRedoTool: 'Undo/Redo',
|
8
8
|
rightClickDragPanTool: 'Right-click drag',
|
9
|
+
pipetteTool: 'Pick color from screen',
|
9
10
|
textTool: 'Text',
|
10
11
|
enterTextToInsert: 'Text to insert',
|
11
12
|
toolEnabledAnnouncement: (toolName) => `${toolName} enabled`,
|
package/dist/src/types.d.ts
CHANGED
@@ -58,7 +58,8 @@ export declare enum EditorEventType {
|
|
58
58
|
ObjectAdded = 4,
|
59
59
|
ViewportChanged = 5,
|
60
60
|
DisplayResized = 6,
|
61
|
-
ColorPickerToggled = 7
|
61
|
+
ColorPickerToggled = 7,
|
62
|
+
ColorPickerColorSelected = 8
|
62
63
|
}
|
63
64
|
declare type EditorToolEventType = EditorEventType.ToolEnabled | EditorEventType.ToolDisabled | EditorEventType.ToolUpdated;
|
64
65
|
export interface EditorToolEvent {
|
@@ -86,7 +87,11 @@ export interface ColorPickerToggled {
|
|
86
87
|
readonly kind: EditorEventType.ColorPickerToggled;
|
87
88
|
readonly open: boolean;
|
88
89
|
}
|
89
|
-
export
|
90
|
+
export interface ColorPickerColorSelected {
|
91
|
+
readonly kind: EditorEventType.ColorPickerColorSelected;
|
92
|
+
readonly color: Color4;
|
93
|
+
}
|
94
|
+
export declare type EditorEventDataType = EditorToolEvent | EditorObjectEvent | EditorViewportChangedEvent | DisplayResizedEvent | EditorUndoStackUpdated | ColorPickerToggled | ColorPickerColorSelected;
|
90
95
|
export declare type OnProgressListener = (amountProcessed: number, totalToProcess: number) => Promise<void> | null;
|
91
96
|
export declare type ComponentAddedListener = (component: AbstractComponent) => void;
|
92
97
|
export declare type OnDetermineExportRectListener = (exportRect: Rect2) => void;
|
package/dist/src/types.js
CHANGED
@@ -18,4 +18,5 @@ export var EditorEventType;
|
|
18
18
|
EditorEventType[EditorEventType["ViewportChanged"] = 5] = "ViewportChanged";
|
19
19
|
EditorEventType[EditorEventType["DisplayResized"] = 6] = "DisplayResized";
|
20
20
|
EditorEventType[EditorEventType["ColorPickerToggled"] = 7] = "ColorPickerToggled";
|
21
|
+
EditorEventType[EditorEventType["ColorPickerColorSelected"] = 8] = "ColorPickerColorSelected";
|
21
22
|
})(EditorEventType || (EditorEventType = {}));
|
package/jest.config.js
CHANGED
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "js-draw",
|
3
|
-
"version": "0.1.
|
3
|
+
"version": "0.1.7",
|
4
4
|
"description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
|
5
5
|
"main": "dist/src/Editor.js",
|
6
6
|
"types": "dist/src/Editor.d.ts",
|
@@ -59,14 +59,14 @@
|
|
59
59
|
"@types/bezier-js": "^4.1.0",
|
60
60
|
"@types/jest": "^28.1.7",
|
61
61
|
"@types/jsdom": "^20.0.0",
|
62
|
-
"@types/node": "^18.7.
|
63
|
-
"@typescript-eslint/eslint-plugin": "^5.
|
64
|
-
"@typescript-eslint/parser": "^5.
|
62
|
+
"@types/node": "^18.7.15",
|
63
|
+
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
64
|
+
"@typescript-eslint/parser": "^5.36.2",
|
65
65
|
"css-loader": "^6.7.1",
|
66
|
-
"eslint": "^8.
|
66
|
+
"eslint": "^8.23.0",
|
67
67
|
"husky": "^8.0.1",
|
68
68
|
"jest": "^28.1.3",
|
69
|
-
"jest-environment-jsdom": "^
|
69
|
+
"jest-environment-jsdom": "^29.0.2",
|
70
70
|
"jsdom": "^20.0.0",
|
71
71
|
"lint-staged": "^13.0.3",
|
72
72
|
"pinst": "^3.0.0",
|
package/src/Color4.ts
CHANGED
@@ -5,14 +5,20 @@ export default class Color4 {
|
|
5
5
|
public readonly g: number,
|
6
6
|
public readonly b: number,
|
7
7
|
public readonly a: number
|
8
|
-
) {
|
8
|
+
) {
|
9
|
+
}
|
9
10
|
|
10
11
|
// Each component should be in the range [0, 1]
|
11
12
|
public static ofRGB(red: number, green: number, blue: number): Color4 {
|
12
|
-
return
|
13
|
+
return Color4.ofRGBA(red, green, blue, 1.0);
|
13
14
|
}
|
14
15
|
|
15
16
|
public static ofRGBA(red: number, green: number, blue: number, alpha: number): Color4 {
|
17
|
+
red = Math.max(0, Math.min(red, 1));
|
18
|
+
green = Math.max(0, Math.min(green, 1));
|
19
|
+
blue = Math.max(0, Math.min(blue, 1));
|
20
|
+
alpha = Math.max(0, Math.min(alpha, 1));
|
21
|
+
|
16
22
|
return new Color4(red, green, blue, alpha);
|
17
23
|
}
|
18
24
|
|
@@ -49,7 +55,7 @@ export default class Color4 {
|
|
49
55
|
throw new Error(`Unable to parse ${hexString}: Wrong number of components.`);
|
50
56
|
}
|
51
57
|
|
52
|
-
return
|
58
|
+
return Color4.ofRGBA(components[0], components[1], components[2], components[3]);
|
53
59
|
}
|
54
60
|
|
55
61
|
// Like fromHex, but can handle additional colors if an HTML5Canvas is available.
|
package/src/Editor.css
CHANGED
@@ -68,3 +68,13 @@
|
|
68
68
|
overflow: hidden;
|
69
69
|
pointer-events: none;
|
70
70
|
}
|
71
|
+
|
72
|
+
.imageEditorContainer .textRendererOutputContainer {
|
73
|
+
width: 1px;
|
74
|
+
height: 1px;
|
75
|
+
overflow: hidden;
|
76
|
+
}
|
77
|
+
|
78
|
+
.imageEditorContainer .textRendererOutputContainer:focus-within {
|
79
|
+
overflow: visible;
|
80
|
+
}
|
package/src/Editor.ts
CHANGED
@@ -108,7 +108,7 @@ export class Editor {
|
|
108
108
|
);
|
109
109
|
|
110
110
|
this.registerListeners();
|
111
|
-
this.
|
111
|
+
this.queueRerender();
|
112
112
|
this.hideLoadingWarning();
|
113
113
|
}
|
114
114
|
|
@@ -277,7 +277,7 @@ export class Editor {
|
|
277
277
|
delta = Vec3.of(0, 0, evt.deltaY);
|
278
278
|
}
|
279
279
|
|
280
|
-
const pos = Vec2.of(evt.
|
280
|
+
const pos = Vec2.of(evt.offsetX, evt.offsetY);
|
281
281
|
if (this.toolController.dispatchInputEvent({
|
282
282
|
kind: InputEvtType.WheelEvt,
|
283
283
|
delta,
|
@@ -307,6 +307,7 @@ export class Editor {
|
|
307
307
|
});
|
308
308
|
}
|
309
309
|
|
310
|
+
// Adds to history by default
|
310
311
|
public dispatch(command: Command, addToHistory: boolean = true) {
|
311
312
|
if (addToHistory) {
|
312
313
|
// .push applies [command] to this
|
@@ -318,6 +319,15 @@ export class Editor {
|
|
318
319
|
this.announceForAccessibility(command.description(this.localization));
|
319
320
|
}
|
320
321
|
|
322
|
+
// Dispatches a command without announcing it. By default, does not add to history.
|
323
|
+
public dispatchNoAnnounce(command: Command, addToHistory: boolean = false) {
|
324
|
+
if (addToHistory) {
|
325
|
+
this.history.push(command);
|
326
|
+
} else {
|
327
|
+
command.apply(this);
|
328
|
+
}
|
329
|
+
}
|
330
|
+
|
321
331
|
// Apply a large transformation in chunks.
|
322
332
|
// If [apply] is false, the commands are unapplied.
|
323
333
|
// Triggers a re-render after each [updateChunkSize]-sized group of commands
|
@@ -486,7 +496,7 @@ export class Editor {
|
|
486
496
|
this.display.setDraftMode(true);
|
487
497
|
|
488
498
|
await loader.start((component) => {
|
489
|
-
(
|
499
|
+
this.dispatchNoAnnounce(EditorImage.addElement(component));
|
490
500
|
}, (countProcessed: number, totalToProcess: number) => {
|
491
501
|
if (countProcessed % 500 === 0) {
|
492
502
|
this.showLoadingWarning(countProcessed / totalToProcess);
|
@@ -498,8 +508,8 @@ export class Editor {
|
|
498
508
|
|
499
509
|
return null;
|
500
510
|
}, (importExportRect: Rect2) => {
|
501
|
-
this.setImportExportRect(importExportRect)
|
502
|
-
this.viewport.zoomTo(importExportRect)
|
511
|
+
this.dispatchNoAnnounce(this.setImportExportRect(importExportRect), false);
|
512
|
+
this.dispatchNoAnnounce(this.viewport.zoomTo(importExportRect), false);
|
503
513
|
});
|
504
514
|
this.hideLoadingWarning();
|
505
515
|
|
@@ -517,22 +527,24 @@ export class Editor {
|
|
517
527
|
const origSize = this.importExportViewport.visibleRect.size;
|
518
528
|
const origTransform = this.importExportViewport.canvasToScreenTransform;
|
519
529
|
|
520
|
-
return {
|
521
|
-
apply(editor) {
|
530
|
+
return new class extends Command {
|
531
|
+
public apply(editor: Editor) {
|
522
532
|
const viewport = editor.importExportViewport;
|
523
533
|
viewport.updateScreenSize(imageRect.size);
|
524
534
|
viewport.resetTransform(Mat33.translation(imageRect.topLeft.times(-1)));
|
525
535
|
editor.queueRerender();
|
526
|
-
}
|
527
|
-
|
536
|
+
}
|
537
|
+
|
538
|
+
public unapply(editor: Editor) {
|
528
539
|
const viewport = editor.importExportViewport;
|
529
540
|
viewport.updateScreenSize(origSize);
|
530
541
|
viewport.resetTransform(origTransform);
|
531
542
|
editor.queueRerender();
|
532
|
-
}
|
533
|
-
|
543
|
+
}
|
544
|
+
|
545
|
+
public description(localizationTable: EditorLocalization) {
|
534
546
|
return localizationTable.resizeOutputCommand(imageRect);
|
535
|
-
}
|
547
|
+
}
|
536
548
|
};
|
537
549
|
}
|
538
550
|
|
package/src/EditorImage.test.ts
CHANGED
@@ -6,8 +6,8 @@ import { Vec2 } from './geometry/Vec2';
|
|
6
6
|
import Path, { PathCommandType } from './geometry/Path';
|
7
7
|
import Color4 from './Color4';
|
8
8
|
import DummyRenderer from './rendering/renderers/DummyRenderer';
|
9
|
-
import { RenderingStyle } from './rendering/renderers/AbstractRenderer';
|
10
9
|
import createEditor from './testing/createEditor';
|
10
|
+
import RenderingStyle from './rendering/RenderingStyle';
|
11
11
|
|
12
12
|
describe('EditorImage', () => {
|
13
13
|
const testStroke = new Stroke([
|
@@ -25,7 +25,7 @@ describe('EditorImage', () => {
|
|
25
25
|
},
|
26
26
|
]);
|
27
27
|
const testFill: RenderingStyle = { fill: Color4.black };
|
28
|
-
const addTestStrokeCommand =
|
28
|
+
const addTestStrokeCommand = EditorImage.addElement(testStroke);
|
29
29
|
|
30
30
|
it('elements added to the image should be findable', () => {
|
31
31
|
const editor = createEditor();
|
@@ -69,7 +69,7 @@ describe('EditorImage', () => {
|
|
69
69
|
|
70
70
|
expect(!leftmostStroke.getBBox().intersects(rightmostStroke.getBBox()));
|
71
71
|
|
72
|
-
(
|
72
|
+
(EditorImage.addElement(leftmostStroke)).apply(editor);
|
73
73
|
|
74
74
|
// The first node should be at the image's root.
|
75
75
|
let firstParent = image.findParent(leftmostStroke);
|
@@ -77,7 +77,7 @@ describe('EditorImage', () => {
|
|
77
77
|
expect(firstParent?.getParent()).toBe(null);
|
78
78
|
expect(firstParent?.getBBox()?.corners).toMatchObject(leftmostStroke.getBBox()?.corners);
|
79
79
|
|
80
|
-
(
|
80
|
+
(EditorImage.addElement(rightmostStroke)).apply(editor);
|
81
81
|
|
82
82
|
firstParent = image.findParent(leftmostStroke);
|
83
83
|
const secondParent = image.findParent(rightmostStroke);
|
package/src/EditorImage.ts
CHANGED
@@ -6,6 +6,7 @@ import AbstractComponent from './components/AbstractComponent';
|
|
6
6
|
import Rect2 from './geometry/Rect2';
|
7
7
|
import { EditorLocalization } from './localization';
|
8
8
|
import RenderingCache from './rendering/caching/RenderingCache';
|
9
|
+
import SerializableCommand from './commands/SerializableCommand';
|
9
10
|
|
10
11
|
export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
|
11
12
|
leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());
|
@@ -14,13 +15,11 @@ export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
|
|
14
15
|
// Handles lookup/storage of elements in the image
|
15
16
|
export default class EditorImage {
|
16
17
|
private root: ImageNode;
|
18
|
+
private componentsById: Record<string, AbstractComponent>;
|
17
19
|
|
18
20
|
public constructor() {
|
19
21
|
this.root = new ImageNode();
|
20
|
-
|
21
|
-
|
22
|
-
private addElement(elem: AbstractComponent): ImageNode {
|
23
|
-
return this.root.addLeaf(elem);
|
22
|
+
this.componentsById = {};
|
24
23
|
}
|
25
24
|
|
26
25
|
// Returns the parent of the given element, if it exists.
|
@@ -59,50 +58,76 @@ export default class EditorImage {
|
|
59
58
|
return leaves.map(leaf => leaf.getContent()!);
|
60
59
|
}
|
61
60
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
61
|
+
public onDestroyElement(elem: AbstractComponent) {
|
62
|
+
delete this.componentsById[elem.getId()];
|
63
|
+
}
|
64
|
+
|
65
|
+
public lookupElement(id: string): AbstractComponent|null {
|
66
|
+
return this.componentsById[id] ?? null;
|
67
|
+
}
|
66
68
|
|
69
|
+
private addElementDirectly(elem: AbstractComponent): ImageNode {
|
70
|
+
this.componentsById[elem.getId()] = elem;
|
71
|
+
return this.root.addLeaf(elem);
|
72
|
+
}
|
73
|
+
|
74
|
+
public static addElement(elem: AbstractComponent, applyByFlattening: boolean = false): Command {
|
75
|
+
return new EditorImage.AddElementCommand(elem, applyByFlattening);
|
76
|
+
}
|
77
|
+
|
78
|
+
// A Command that can access private [EditorImage] functionality
|
79
|
+
private static AddElementCommand = class extends SerializableCommand {
|
67
80
|
// If [applyByFlattening], then the rendered content of this element
|
68
81
|
// is present on the display's wet ink canvas. As such, no re-render is necessary
|
69
82
|
// the first time this command is applied (the surfaces are joined instead).
|
70
83
|
public constructor(
|
71
|
-
element: AbstractComponent,
|
72
|
-
applyByFlattening: boolean = false
|
84
|
+
private element: AbstractComponent,
|
85
|
+
private applyByFlattening: boolean = false
|
73
86
|
) {
|
74
|
-
|
75
|
-
this.#applyByFlattening = applyByFlattening;
|
87
|
+
super('add-element');
|
76
88
|
|
77
|
-
if (isNaN(
|
89
|
+
if (isNaN(element.getBBox().area)) {
|
78
90
|
throw new Error('Elements in the image cannot have NaN bounding boxes');
|
79
91
|
}
|
80
92
|
}
|
81
93
|
|
82
94
|
public apply(editor: Editor) {
|
83
|
-
editor.image.
|
95
|
+
editor.image.addElementDirectly(this.element);
|
84
96
|
|
85
|
-
if (!this
|
97
|
+
if (!this.applyByFlattening) {
|
86
98
|
editor.queueRerender();
|
87
99
|
} else {
|
88
|
-
this
|
100
|
+
this.applyByFlattening = false;
|
89
101
|
editor.display.flatten();
|
90
102
|
}
|
91
103
|
}
|
92
104
|
|
93
105
|
public unapply(editor: Editor) {
|
94
|
-
const container = editor.image.findParent(this
|
106
|
+
const container = editor.image.findParent(this.element);
|
95
107
|
container?.remove();
|
96
108
|
editor.queueRerender();
|
97
109
|
}
|
98
110
|
|
99
111
|
public description(localization: EditorLocalization) {
|
100
|
-
return localization.addElementAction(this
|
112
|
+
return localization.addElementAction(this.element.description(localization));
|
113
|
+
}
|
114
|
+
|
115
|
+
protected serializeToString() {
|
116
|
+
return JSON.stringify({
|
117
|
+
elemData: this.element.serialize(),
|
118
|
+
});
|
119
|
+
}
|
120
|
+
|
121
|
+
static {
|
122
|
+
SerializableCommand.register('add-element', (data: string, _editor: Editor) => {
|
123
|
+
const json = JSON.parse(data);
|
124
|
+
const elem = AbstractComponent.deserialize(json.elemData);
|
125
|
+
return new EditorImage.AddElementCommand(elem);
|
126
|
+
});
|
101
127
|
}
|
102
128
|
};
|
103
129
|
}
|
104
130
|
|
105
|
-
export type AddElementCommand = typeof EditorImage.AddElementCommand.prototype;
|
106
131
|
type TooSmallToRenderCheck = (rect: Rect2)=> boolean;
|
107
132
|
|
108
133
|
// TODO: Assign leaf nodes to CacheNodes. When leaf nodes are modified, the corresponding CacheNodes can be updated.
|
@@ -221,6 +246,7 @@ export class ImageNode {
|
|
221
246
|
nodeForChildren.children = this.children;
|
222
247
|
this.children = [nodeForNewLeaf, nodeForChildren];
|
223
248
|
nodeForChildren.recomputeBBox(true);
|
249
|
+
nodeForChildren.updateParents();
|
224
250
|
|
225
251
|
return nodeForNewLeaf.addLeaf(leaf);
|
226
252
|
}
|
@@ -279,6 +305,16 @@ export class ImageNode {
|
|
279
305
|
}
|
280
306
|
}
|
281
307
|
|
308
|
+
private updateParents(recursive: boolean = false) {
|
309
|
+
for (const child of this.children) {
|
310
|
+
child.parent = this;
|
311
|
+
|
312
|
+
if (recursive) {
|
313
|
+
child.updateParents(recursive);
|
314
|
+
}
|
315
|
+
}
|
316
|
+
}
|
317
|
+
|
282
318
|
private rebalance() {
|
283
319
|
// If the current node is its parent's only child,
|
284
320
|
if (this.parent && this.parent.children.length === 1) {
|
@@ -296,6 +332,7 @@ export class ImageNode {
|
|
296
332
|
} else if (this.content === null) {
|
297
333
|
// Remove this and transfer this' children to the parent.
|
298
334
|
this.parent.children = this.children;
|
335
|
+
this.parent.updateParents();
|
299
336
|
this.parent = null;
|
300
337
|
}
|
301
338
|
}
|
@@ -314,7 +351,11 @@ export class ImageNode {
|
|
314
351
|
this.parent.children = this.parent.children.filter(node => {
|
315
352
|
return node !== this;
|
316
353
|
});
|
317
|
-
|
354
|
+
|
355
|
+
console.assert(
|
356
|
+
this.parent.children.length === oldChildCount - 1,
|
357
|
+
`${oldChildCount - 1} ≠ ${this.parent.children.length} after removing all nodes equal to ${this}. Nodes should only be removed once.`
|
358
|
+
);
|
318
359
|
|
319
360
|
this.parent.children.forEach(child => {
|
320
361
|
child.rebalance();
|
package/src/SVGLoader.ts
CHANGED
@@ -8,7 +8,8 @@ import Mat33 from './geometry/Mat33';
|
|
8
8
|
import Path from './geometry/Path';
|
9
9
|
import Rect2 from './geometry/Rect2';
|
10
10
|
import { Vec2 } from './geometry/Vec2';
|
11
|
-
import { RenderablePathSpec
|
11
|
+
import { RenderablePathSpec } from './rendering/renderers/AbstractRenderer';
|
12
|
+
import RenderingStyle from './rendering/RenderingStyle';
|
12
13
|
import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
|
13
14
|
|
14
15
|
type OnFinishListener = ()=> void;
|
@@ -198,14 +199,19 @@ export default class SVGLoader implements ImageLoader {
|
|
198
199
|
}
|
199
200
|
const style: TextStyle = {
|
200
201
|
size: fontSize,
|
201
|
-
fontFamily: computedStyles.fontFamily || 'sans-serif',
|
202
|
+
fontFamily: computedStyles.fontFamily || elem.style.fontFamily || 'sans-serif',
|
202
203
|
renderingStyle: {
|
203
204
|
fill: Color4.fromString(computedStyles.fill)
|
204
205
|
},
|
205
206
|
};
|
206
207
|
|
208
|
+
let transformProperty = computedStyles.transform;
|
209
|
+
if (transformProperty === '' || transformProperty === 'none') {
|
210
|
+
transformProperty = elem.style.transform || 'none';
|
211
|
+
}
|
212
|
+
|
207
213
|
// Compute transform matrix
|
208
|
-
let transform = Mat33.fromCSSMatrix(
|
214
|
+
let transform = Mat33.fromCSSMatrix(transformProperty);
|
209
215
|
const supportedAttrs = [];
|
210
216
|
const elemX = elem.getAttribute('x');
|
211
217
|
const elemY = elem.getAttribute('y');
|
package/src/Viewport.ts
CHANGED
@@ -13,10 +13,11 @@ type PointDataType<T extends Point2|StrokeDataPoint|number> = T extends Point2 ?
|
|
13
13
|
|
14
14
|
export class Viewport {
|
15
15
|
// Command that translates/scales the viewport.
|
16
|
-
public static ViewportTransform = class
|
16
|
+
public static ViewportTransform = class extends Command {
|
17
17
|
readonly #inverseTransform: Mat33;
|
18
18
|
|
19
19
|
public constructor(public readonly transform: Mat33) {
|
20
|
+
super();
|
20
21
|
this.#inverseTransform = transform.inverse();
|
21
22
|
}
|
22
23
|
|
@@ -101,7 +102,7 @@ export class Viewport {
|
|
101
102
|
|
102
103
|
// Updates the transformation directly. Using ViewportTransform is preferred.
|
103
104
|
// [newTransform] should map from canvas coordinates to screen coordinates.
|
104
|
-
public resetTransform(newTransform: Mat33) {
|
105
|
+
public resetTransform(newTransform: Mat33 = Mat33.identity) {
|
105
106
|
this.transform = newTransform;
|
106
107
|
this.inverseTransform = newTransform.inverse();
|
107
108
|
this.notifier.dispatch(EditorEventType.ViewportChanged, {
|
@@ -181,19 +182,19 @@ export class Viewport {
|
|
181
182
|
throw new Error(`${toMakeVisible.toString()} rectangle has NaN size! Cannot zoom to!`);
|
182
183
|
}
|
183
184
|
|
184
|
-
// Try to move the selection within the center
|
185
|
+
// Try to move the selection within the center 4/5ths of the viewport.
|
185
186
|
const recomputeTargetRect = () => {
|
186
187
|
// transform transforms objects on the canvas. As such, we need to invert it
|
187
188
|
// to transform the viewport.
|
188
189
|
const visibleRect = this.visibleRect.transformedBoundingBox(transform.inverse());
|
189
|
-
return visibleRect.transformedBoundingBox(Mat33.scaling2D(
|
190
|
+
return visibleRect.transformedBoundingBox(Mat33.scaling2D(4/5, visibleRect.center));
|
190
191
|
};
|
191
192
|
|
192
193
|
let targetRect = recomputeTargetRect();
|
193
194
|
const largerThanTarget = targetRect.w < toMakeVisible.w || targetRect.h < toMakeVisible.h;
|
194
195
|
|
195
|
-
// Ensure that toMakeVisible is at least 1/
|
196
|
-
const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension <
|
196
|
+
// Ensure that toMakeVisible is at least 1/3rd of the visible region.
|
197
|
+
const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 1/3;
|
197
198
|
|
198
199
|
if ((largerThanTarget && allowZoomOut) || (muchSmallerThanTarget && allowZoomIn)) {
|
199
200
|
// If larger than the target, ensure that the longest axis is visible.
|
package/src/commands/Command.ts
CHANGED
@@ -1,32 +1,28 @@
|
|
1
1
|
import Editor from '../Editor';
|
2
2
|
import { EditorLocalization } from '../localization';
|
3
3
|
|
4
|
-
|
5
|
-
apply(editor: Editor): void;
|
6
|
-
unapply(editor: Editor): void;
|
4
|
+
export abstract class Command {
|
5
|
+
public abstract apply(editor: Editor): void;
|
6
|
+
public abstract unapply(editor: Editor): void;
|
7
7
|
|
8
|
-
|
9
|
-
}
|
8
|
+
// Called when the command is being deleted
|
9
|
+
public onDrop(_editor: Editor) { }
|
10
10
|
|
11
|
-
|
12
|
-
namespace Command {
|
13
|
-
export const empty = {
|
14
|
-
apply(_editor: Editor) { },
|
15
|
-
unapply(_editor: Editor) { },
|
16
|
-
};
|
11
|
+
public abstract description(localizationTable: EditorLocalization): string;
|
17
12
|
|
18
|
-
|
19
|
-
return {
|
20
|
-
apply(editor: Editor) {
|
13
|
+
public static union(a: Command, b: Command): Command {
|
14
|
+
return new class extends Command {
|
15
|
+
public apply(editor: Editor) {
|
21
16
|
a.apply(editor);
|
22
17
|
b.apply(editor);
|
23
|
-
}
|
24
|
-
|
18
|
+
}
|
19
|
+
|
20
|
+
public unapply(editor: Editor) {
|
25
21
|
b.unapply(editor);
|
26
22
|
a.unapply(editor);
|
27
|
-
}
|
23
|
+
}
|
28
24
|
|
29
|
-
description(localizationTable: EditorLocalization) {
|
25
|
+
public description(localizationTable: EditorLocalization) {
|
30
26
|
const aDescription = a.description(localizationTable);
|
31
27
|
const bDescription = b.description(localizationTable);
|
32
28
|
|
@@ -35,8 +31,14 @@ namespace Command {
|
|
35
31
|
}
|
36
32
|
|
37
33
|
return `${aDescription}, ${bDescription}`;
|
38
|
-
}
|
34
|
+
}
|
39
35
|
};
|
36
|
+
}
|
37
|
+
|
38
|
+
public static readonly empty = new class extends Command {
|
39
|
+
public description(_localizationTable: EditorLocalization) { return ''; }
|
40
|
+
public apply(_editor: Editor) { }
|
41
|
+
public unapply(_editor: Editor) { }
|
40
42
|
};
|
41
43
|
}
|
42
44
|
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import AbstractComponent from '../components/AbstractComponent';
|
2
|
+
import describeComponentList from '../components/util/describeComponentList';
|
3
|
+
import Editor from '../Editor';
|
4
|
+
import { EditorLocalization } from '../localization';
|
5
|
+
import Erase from './Erase';
|
6
|
+
import SerializableCommand from './SerializableCommand';
|
7
|
+
|
8
|
+
export default class Duplicate extends SerializableCommand {
|
9
|
+
private duplicates: AbstractComponent[];
|
10
|
+
private reverse: Erase;
|
11
|
+
|
12
|
+
public constructor(private toDuplicate: AbstractComponent[]) {
|
13
|
+
super('duplicate');
|
14
|
+
|
15
|
+
this.duplicates = toDuplicate.map(elem => elem.clone());
|
16
|
+
this.reverse = new Erase(this.duplicates);
|
17
|
+
}
|
18
|
+
|
19
|
+
public apply(editor: Editor): void {
|
20
|
+
this.reverse.unapply(editor);
|
21
|
+
}
|
22
|
+
|
23
|
+
public unapply(editor: Editor): void {
|
24
|
+
this.reverse.apply(editor);
|
25
|
+
}
|
26
|
+
|
27
|
+
public description(localizationTable: EditorLocalization): string {
|
28
|
+
if (this.duplicates.length === 0) {
|
29
|
+
return localizationTable.duplicatedNoElements;
|
30
|
+
}
|
31
|
+
|
32
|
+
return localizationTable.duplicateAction(
|
33
|
+
describeComponentList(localizationTable, this.duplicates) ?? localizationTable.elements,
|
34
|
+
this.duplicates.length
|
35
|
+
);
|
36
|
+
}
|
37
|
+
|
38
|
+
protected serializeToString(): string {
|
39
|
+
return JSON.stringify(this.toDuplicate.map(elem => elem.getId()));
|
40
|
+
}
|
41
|
+
|
42
|
+
static {
|
43
|
+
SerializableCommand.register('duplicate', (data: string, editor: Editor) => {
|
44
|
+
const json = JSON.parse(data);
|
45
|
+
const elems = json.map((id: string) => editor.image.lookupElement(id));
|
46
|
+
return new Duplicate(elems);
|
47
|
+
});
|
48
|
+
}
|
49
|
+
}
|