js-draw 1.16.1 → 1.18.0
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +70 -10
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +75 -7
- package/dist/cjs/Editor.js +93 -90
- package/dist/cjs/Pointer.d.ts +2 -1
- package/dist/cjs/Pointer.js +9 -2
- package/dist/cjs/commands/localization.d.ts +1 -0
- package/dist/cjs/commands/localization.js +1 -0
- package/dist/cjs/commands/uniteCommands.d.ts +5 -1
- package/dist/cjs/commands/uniteCommands.js +33 -7
- package/dist/cjs/components/AbstractComponent.d.ts +17 -5
- package/dist/cjs/components/AbstractComponent.js +15 -15
- package/dist/cjs/components/Stroke.d.ts +4 -1
- package/dist/cjs/components/Stroke.js +158 -2
- package/dist/cjs/components/TextComponent.d.ts +36 -1
- package/dist/cjs/components/TextComponent.js +39 -1
- package/dist/cjs/components/builders/ArrowBuilder.js +1 -1
- package/dist/cjs/components/builders/PolylineBuilder.d.ts +35 -0
- package/dist/cjs/components/builders/PolylineBuilder.js +122 -0
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +44 -51
- package/dist/cjs/components/builders/autocorrect/makeShapeFitAutocorrect.js +1 -1
- package/dist/cjs/components/lib.d.ts +1 -0
- package/dist/cjs/components/lib.js +3 -1
- package/dist/cjs/components/util/StrokeSmoother.js +4 -4
- package/dist/cjs/image/EditorImage.d.ts +4 -1
- package/dist/cjs/image/EditorImage.js +5 -2
- package/dist/cjs/inputEvents.d.ts +11 -1
- package/dist/cjs/localizations/comments.d.ts +3 -0
- package/dist/cjs/localizations/comments.js +3 -0
- package/dist/cjs/localizations/de.js +1 -3
- package/dist/cjs/localizations/es.js +3 -3
- package/dist/cjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
- package/dist/cjs/rendering/renderers/CanvasRenderer.js +16 -0
- package/dist/cjs/rendering/renderers/SVGRenderer.js +1 -1
- package/dist/cjs/testing/createEditor.d.ts +2 -2
- package/dist/cjs/testing/createEditor.js +2 -2
- package/dist/cjs/toolbar/IconProvider.d.ts +9 -4
- package/dist/cjs/toolbar/IconProvider.js +21 -7
- package/dist/cjs/toolbar/localization.d.ts +6 -1
- package/dist/cjs/toolbar/localization.js +7 -2
- package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +24 -1
- package/dist/cjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
- package/dist/cjs/toolbar/widgets/EraserToolWidget.js +45 -5
- package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
- package/dist/cjs/toolbar/widgets/PenToolWidget.js +17 -4
- package/dist/cjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
- package/dist/cjs/toolbar/widgets/keybindings.js +1 -1
- package/dist/cjs/tools/Eraser.d.ts +24 -4
- package/dist/cjs/tools/Eraser.js +108 -21
- package/dist/cjs/tools/InputFilter/InputStabilizer.js +3 -3
- package/dist/cjs/tools/PasteHandler.js +35 -10
- package/dist/cjs/tools/Pen.js +2 -2
- package/dist/cjs/tools/SelectionTool/SelectionTool.js +23 -4
- package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +1 -1
- package/dist/cjs/tools/ToolController.d.ts +17 -1
- package/dist/cjs/tools/ToolController.js +21 -8
- package/dist/cjs/tools/lib.d.ts +1 -4
- package/dist/cjs/tools/lib.js +2 -4
- package/dist/cjs/tools/localization.d.ts +2 -2
- package/dist/cjs/tools/localization.js +2 -2
- package/dist/cjs/util/ClipboardHandler.d.ts +27 -0
- package/dist/cjs/util/ClipboardHandler.js +205 -0
- package/dist/cjs/util/ClipboardHandler.test.d.ts +1 -0
- package/dist/cjs/version.d.ts +5 -0
- package/dist/cjs/version.js +6 -1
- package/dist/mjs/Editor.d.ts +75 -7
- package/dist/mjs/Editor.mjs +93 -90
- package/dist/mjs/Pointer.d.ts +2 -1
- package/dist/mjs/Pointer.mjs +9 -2
- package/dist/mjs/commands/localization.d.ts +1 -0
- package/dist/mjs/commands/localization.mjs +1 -0
- package/dist/mjs/commands/uniteCommands.d.ts +5 -1
- package/dist/mjs/commands/uniteCommands.mjs +33 -7
- package/dist/mjs/components/AbstractComponent.d.ts +17 -5
- package/dist/mjs/components/AbstractComponent.mjs +15 -15
- package/dist/mjs/components/Stroke.d.ts +4 -1
- package/dist/mjs/components/Stroke.mjs +159 -3
- package/dist/mjs/components/TextComponent.d.ts +36 -1
- package/dist/mjs/components/TextComponent.mjs +40 -2
- package/dist/mjs/components/builders/ArrowBuilder.mjs +1 -1
- package/dist/mjs/components/builders/PolylineBuilder.d.ts +35 -0
- package/dist/mjs/components/builders/PolylineBuilder.mjs +115 -0
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.d.ts +1 -1
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +45 -52
- package/dist/mjs/components/builders/autocorrect/makeShapeFitAutocorrect.mjs +1 -1
- package/dist/mjs/components/lib.d.ts +1 -0
- package/dist/mjs/components/lib.mjs +1 -0
- package/dist/mjs/components/util/StrokeSmoother.mjs +4 -4
- package/dist/mjs/image/EditorImage.d.ts +4 -1
- package/dist/mjs/image/EditorImage.mjs +5 -2
- package/dist/mjs/inputEvents.d.ts +11 -1
- package/dist/mjs/localizations/comments.d.ts +3 -0
- package/dist/mjs/localizations/comments.mjs +3 -0
- package/dist/mjs/localizations/de.mjs +1 -3
- package/dist/mjs/localizations/es.mjs +3 -3
- package/dist/mjs/rendering/renderers/CanvasRenderer.d.ts +7 -0
- package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +16 -0
- package/dist/mjs/rendering/renderers/SVGRenderer.mjs +1 -1
- package/dist/mjs/testing/createEditor.d.ts +2 -2
- package/dist/mjs/testing/createEditor.mjs +2 -2
- package/dist/mjs/toolbar/IconProvider.d.ts +9 -4
- package/dist/mjs/toolbar/IconProvider.mjs +21 -7
- package/dist/mjs/toolbar/localization.d.ts +6 -1
- package/dist/mjs/toolbar/localization.mjs +7 -2
- package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +24 -1
- package/dist/mjs/toolbar/widgets/EraserToolWidget.d.ts +6 -1
- package/dist/mjs/toolbar/widgets/EraserToolWidget.mjs +47 -6
- package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
- package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +17 -4
- package/dist/mjs/toolbar/widgets/PenToolWidget.test.d.ts +1 -0
- package/dist/mjs/toolbar/widgets/keybindings.mjs +1 -1
- package/dist/mjs/tools/Eraser.d.ts +24 -4
- package/dist/mjs/tools/Eraser.mjs +108 -22
- package/dist/mjs/tools/InputFilter/InputStabilizer.mjs +3 -3
- package/dist/mjs/tools/PasteHandler.mjs +35 -10
- package/dist/mjs/tools/Pen.mjs +2 -2
- package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +23 -4
- package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +1 -1
- package/dist/mjs/tools/ToolController.d.ts +17 -1
- package/dist/mjs/tools/ToolController.mjs +21 -8
- package/dist/mjs/tools/lib.d.ts +1 -4
- package/dist/mjs/tools/lib.mjs +1 -4
- package/dist/mjs/tools/localization.d.ts +2 -2
- package/dist/mjs/tools/localization.mjs +2 -2
- package/dist/mjs/util/ClipboardHandler.d.ts +27 -0
- package/dist/mjs/util/ClipboardHandler.mjs +200 -0
- package/dist/mjs/util/ClipboardHandler.test.d.ts +1 -0
- package/dist/mjs/version.d.ts +5 -0
- package/dist/mjs/version.mjs +6 -1
- package/package.json +6 -6
package/dist/cjs/Editor.js
CHANGED
@@ -42,7 +42,6 @@ const getLocalizationTable_1 = __importDefault(require("./localizations/getLocal
|
|
42
42
|
const IconProvider_1 = __importDefault(require("./toolbar/IconProvider"));
|
43
43
|
const CanvasRenderer_1 = __importDefault(require("./rendering/renderers/CanvasRenderer"));
|
44
44
|
const untilNextAnimationFrame_1 = __importDefault(require("./util/untilNextAnimationFrame"));
|
45
|
-
const fileToBase64Url_1 = __importDefault(require("./util/fileToBase64Url"));
|
46
45
|
const uniteCommands_1 = __importDefault(require("./commands/uniteCommands"));
|
47
46
|
const SelectionTool_1 = __importDefault(require("./tools/SelectionTool/SelectionTool"));
|
48
47
|
const Erase_1 = __importDefault(require("./commands/Erase"));
|
@@ -58,6 +57,7 @@ const editorImageToSVG_1 = require("./image/export/editorImageToSVG");
|
|
58
57
|
const ReactiveValue_1 = require("./util/ReactiveValue");
|
59
58
|
const listenForKeyboardEventsFrom_1 = __importDefault(require("./util/listenForKeyboardEventsFrom"));
|
60
59
|
const mitLicenseAttribution_1 = __importDefault(require("./util/mitLicenseAttribution"));
|
60
|
+
const ClipboardHandler_1 = __importDefault(require("./util/ClipboardHandler"));
|
61
61
|
/**
|
62
62
|
* The main entrypoint for the full editor.
|
63
63
|
*
|
@@ -76,7 +76,7 @@ const mitLicenseAttribution_1 = __importDefault(require("./util/mitLicenseAttrib
|
|
76
76
|
* ```
|
77
77
|
*
|
78
78
|
* See also
|
79
|
-
* [`
|
79
|
+
* * [`examples.md`](https://github.com/personalizedrefrigerator/js-draw/blob/main/docs/examples.md).
|
80
80
|
*/
|
81
81
|
class Editor {
|
82
82
|
/**
|
@@ -138,6 +138,10 @@ class Editor {
|
|
138
138
|
iconProvider: settings.iconProvider ?? new IconProvider_1.default(),
|
139
139
|
notices: [],
|
140
140
|
appInfo: settings.appInfo ? { ...settings.appInfo } : null,
|
141
|
+
pens: {
|
142
|
+
additionalPenTypes: settings.pens?.additionalPenTypes ?? [],
|
143
|
+
filterPenTypes: settings.pens?.filterPenTypes ?? (() => true)
|
144
|
+
},
|
141
145
|
};
|
142
146
|
// Validate settings
|
143
147
|
if (this.settings.minZoom > this.settings.maxZoom) {
|
@@ -218,6 +222,16 @@ class Editor {
|
|
218
222
|
}
|
219
223
|
});
|
220
224
|
}
|
225
|
+
/**
|
226
|
+
* @returns a shallow copy of the current settings of the editor.
|
227
|
+
*
|
228
|
+
* Do not modify.
|
229
|
+
*/
|
230
|
+
getCurrentSettings() {
|
231
|
+
return {
|
232
|
+
...this.settings,
|
233
|
+
};
|
234
|
+
}
|
221
235
|
/**
|
222
236
|
* @returns a reference to the editor's container.
|
223
237
|
*
|
@@ -304,19 +318,12 @@ class Editor {
|
|
304
318
|
this.accessibilityControlArea.addEventListener('input', () => {
|
305
319
|
this.accessibilityControlArea.value = '';
|
306
320
|
});
|
307
|
-
|
321
|
+
const copyHandler = new ClipboardHandler_1.default(this);
|
322
|
+
document.addEventListener('copy', async (evt) => {
|
308
323
|
if (!this.isEventSink(document.querySelector(':focus'))) {
|
309
324
|
return;
|
310
325
|
}
|
311
|
-
|
312
|
-
if (this.toolController.dispatchInputEvent({
|
313
|
-
kind: inputEvents_1.InputEvtType.CopyEvent,
|
314
|
-
setData: (mime, data) => {
|
315
|
-
clipboardData?.setData(mime, data);
|
316
|
-
},
|
317
|
-
})) {
|
318
|
-
evt.preventDefault();
|
319
|
-
}
|
326
|
+
copyHandler.copy(evt);
|
320
327
|
});
|
321
328
|
document.addEventListener('paste', evt => {
|
322
329
|
this.handlePaste(evt);
|
@@ -384,11 +391,21 @@ class Editor {
|
|
384
391
|
* (e.g. with synthetic events). @internal
|
385
392
|
*/
|
386
393
|
setPointerCapture(target, pointerId) {
|
387
|
-
|
394
|
+
try {
|
395
|
+
target.setPointerCapture(pointerId);
|
396
|
+
}
|
397
|
+
catch (error) {
|
398
|
+
console.warn('Failed to setPointerCapture', error);
|
399
|
+
}
|
388
400
|
}
|
389
401
|
/** Can be overridden in a testing environment to handle synthetic events. @internal */
|
390
402
|
releasePointerCapture(target, pointerId) {
|
391
|
-
|
403
|
+
try {
|
404
|
+
target.releasePointerCapture(pointerId);
|
405
|
+
}
|
406
|
+
catch (error) {
|
407
|
+
console.warn('Failed to releasePointerCapture', error);
|
408
|
+
}
|
392
409
|
}
|
393
410
|
/**
|
394
411
|
* Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
|
@@ -414,7 +431,7 @@ class Editor {
|
|
414
431
|
if (pointer.down) {
|
415
432
|
const prevData = this.pointers[pointer.id];
|
416
433
|
if (prevData) {
|
417
|
-
const distanceMoved = pointer.screenPos.
|
434
|
+
const distanceMoved = pointer.screenPos.distanceTo(prevData.screenPos);
|
418
435
|
// If the pointer moved less than two pixels, don't send a new event.
|
419
436
|
if (distanceMoved < 2) {
|
420
437
|
return false;
|
@@ -473,66 +490,7 @@ class Editor {
|
|
473
490
|
if (!this.isEventSink(target)) {
|
474
491
|
return;
|
475
492
|
}
|
476
|
-
|
477
|
-
if (!clipboardData) {
|
478
|
-
return;
|
479
|
-
}
|
480
|
-
// Handle SVG files (prefer to PNG/JPEG)
|
481
|
-
for (const file of clipboardData.files) {
|
482
|
-
if (file.type.toLowerCase() === 'image/svg+xml') {
|
483
|
-
const text = await file.text();
|
484
|
-
if (this.toolController.dispatchInputEvent({
|
485
|
-
kind: inputEvents_1.InputEvtType.PasteEvent,
|
486
|
-
mime: file.type,
|
487
|
-
data: text,
|
488
|
-
})) {
|
489
|
-
evt.preventDefault();
|
490
|
-
return;
|
491
|
-
}
|
492
|
-
}
|
493
|
-
}
|
494
|
-
// Handle image files.
|
495
|
-
for (const file of clipboardData.files) {
|
496
|
-
const fileType = file.type.toLowerCase();
|
497
|
-
if (fileType === 'image/png' || fileType === 'image/jpg') {
|
498
|
-
this.showLoadingWarning(0);
|
499
|
-
const onprogress = (evt) => {
|
500
|
-
this.showLoadingWarning(evt.loaded / evt.total);
|
501
|
-
};
|
502
|
-
try {
|
503
|
-
const data = await (0, fileToBase64Url_1.default)(file, { onprogress });
|
504
|
-
if (data && this.toolController.dispatchInputEvent({
|
505
|
-
kind: inputEvents_1.InputEvtType.PasteEvent,
|
506
|
-
mime: fileType,
|
507
|
-
data: data,
|
508
|
-
})) {
|
509
|
-
evt.preventDefault();
|
510
|
-
this.hideLoadingWarning();
|
511
|
-
return;
|
512
|
-
}
|
513
|
-
}
|
514
|
-
catch (e) {
|
515
|
-
console.error('Error reading image:', e);
|
516
|
-
}
|
517
|
-
this.hideLoadingWarning();
|
518
|
-
}
|
519
|
-
}
|
520
|
-
// Supported MIMEs for text data, in order of preference
|
521
|
-
const supportedMIMEs = [
|
522
|
-
'image/svg+xml',
|
523
|
-
'text/plain',
|
524
|
-
];
|
525
|
-
for (const mime of supportedMIMEs) {
|
526
|
-
const data = clipboardData.getData(mime);
|
527
|
-
if (data && this.toolController.dispatchInputEvent({
|
528
|
-
kind: inputEvents_1.InputEvtType.PasteEvent,
|
529
|
-
mime,
|
530
|
-
data,
|
531
|
-
})) {
|
532
|
-
evt.preventDefault();
|
533
|
-
return;
|
534
|
-
}
|
535
|
-
}
|
493
|
+
return await new ClipboardHandler_1.default(this).paste(evt);
|
536
494
|
}
|
537
495
|
/**
|
538
496
|
* Forward pointer events from `elem` to this editor. Such that right-click/right-click drag
|
@@ -643,7 +601,7 @@ class Editor {
|
|
643
601
|
const eventBuffer = gestureData[pointerId].eventBuffer;
|
644
602
|
// Skip if the pointer hasn't moved enough to not be a "click".
|
645
603
|
const strokeStartThreshold = 10;
|
646
|
-
const isWithinClickThreshold = gestureStartPos && currentPos.
|
604
|
+
const isWithinClickThreshold = gestureStartPos && currentPos.distanceTo(gestureStartPos) < strokeStartThreshold;
|
647
605
|
if (isWithinClickThreshold && !gestureData[pointerId].hasMovedSignificantly) {
|
648
606
|
eventBuffer.push([eventName, event]);
|
649
607
|
sendToEditor = false;
|
@@ -757,7 +715,8 @@ class Editor {
|
|
757
715
|
/** `apply` a command. `command` will be announced for accessibility. */
|
758
716
|
dispatch(command, addToHistory = true) {
|
759
717
|
const dispatchResult = this.dispatchNoAnnounce(command, addToHistory);
|
760
|
-
|
718
|
+
const commandDescription = command.description(this, this.localization);
|
719
|
+
this.announceForAccessibility(commandDescription);
|
761
720
|
return dispatchResult;
|
762
721
|
}
|
763
722
|
/**
|
@@ -971,8 +930,11 @@ class Editor {
|
|
971
930
|
* This is a convenience method that creates **and applies** a single command.
|
972
931
|
*
|
973
932
|
* If `selectComponents` is true (the default), the components are selected.
|
933
|
+
*
|
934
|
+
* `actionDescription`, if given, should be a screenreader-friendly description of the
|
935
|
+
* reason components were added (e.g. "pasted").
|
974
936
|
*/
|
975
|
-
async addAndCenterComponents(components, selectComponents = true) {
|
937
|
+
async addAndCenterComponents(components, selectComponents = true, actionDescription) {
|
976
938
|
let bbox = null;
|
977
939
|
for (const component of components) {
|
978
940
|
if (bbox) {
|
@@ -1003,7 +965,7 @@ class Editor {
|
|
1003
965
|
commands.push(component.transformBy(transfm));
|
1004
966
|
}
|
1005
967
|
const applyChunkSize = 100;
|
1006
|
-
await this.dispatch((0, uniteCommands_1.default)(commands, applyChunkSize), true);
|
968
|
+
await this.dispatch((0, uniteCommands_1.default)(commands, { applyChunkSize, description: actionDescription }), true);
|
1007
969
|
if (selectComponents) {
|
1008
970
|
for (const selectionTool of this.toolController.getMatchingTools(SelectionTool_1.default)) {
|
1009
971
|
selectionTool.setEnabled(true);
|
@@ -1023,17 +985,7 @@ class Editor {
|
|
1023
985
|
* [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]]
|
1024
986
|
*/
|
1025
987
|
toDataURL(format = 'image/png', outputSize) {
|
1026
|
-
const canvas =
|
1027
|
-
const importExportViewport = this.image.getImportExportViewport();
|
1028
|
-
const exportRectSize = importExportViewport.getScreenRectSize();
|
1029
|
-
const resolution = outputSize ?? exportRectSize;
|
1030
|
-
canvas.width = resolution.x;
|
1031
|
-
canvas.height = resolution.y;
|
1032
|
-
const ctx = canvas.getContext('2d');
|
1033
|
-
// Scale to ensure that the entire output is visible.
|
1034
|
-
const scaleFactor = Math.min(resolution.x / exportRectSize.x, resolution.y / exportRectSize.y);
|
1035
|
-
ctx.scale(scaleFactor, scaleFactor);
|
1036
|
-
const renderer = new CanvasRenderer_1.default(ctx, importExportViewport);
|
988
|
+
const { element: canvas, renderer } = CanvasRenderer_1.default.fromViewport(this.image.getImportExportViewport(), { canvasSize: outputSize });
|
1037
989
|
this.image.renderAll(renderer);
|
1038
990
|
const dataURL = canvas.toDataURL(format);
|
1039
991
|
return dataURL;
|
@@ -1126,8 +1078,59 @@ class Editor {
|
|
1126
1078
|
}
|
1127
1079
|
return background;
|
1128
1080
|
}
|
1081
|
+
/**
|
1082
|
+
* This is a convenience method for adding or updating the {@link BackgroundComponent}
|
1083
|
+
* and {@link EditorImage.setAutoresizeEnabled} for the current image.
|
1084
|
+
*
|
1085
|
+
* If there are multiple {@link BackgroundComponent}s in the image, this only modifies
|
1086
|
+
* the topmost such element.
|
1087
|
+
*
|
1088
|
+
* **Example**:
|
1089
|
+
* ```ts,runnable
|
1090
|
+
* import { Editor, Color4, BackgroundComponentBackgroundType } from 'js-draw';
|
1091
|
+
* const editor = new Editor(document.body);
|
1092
|
+
* editor.dispatch(editor.setBackgroundStyle({
|
1093
|
+
* color: Color4.orange,
|
1094
|
+
* type: BackgroundComponentBackgroundType.Grid,
|
1095
|
+
* autoresize: true,
|
1096
|
+
* }));
|
1097
|
+
* ```
|
1098
|
+
*
|
1099
|
+
* To change the background size, see {@link EditorImage.setImportExportRect}.
|
1100
|
+
*/
|
1101
|
+
setBackgroundStyle(style) {
|
1102
|
+
const originalBackground = this.getTopmostBackgroundComponent();
|
1103
|
+
const commands = [];
|
1104
|
+
if (originalBackground) {
|
1105
|
+
commands.push(new Erase_1.default([originalBackground]));
|
1106
|
+
}
|
1107
|
+
const originalType = originalBackground?.getBackgroundType?.() ?? BackgroundComponent_1.BackgroundType.None;
|
1108
|
+
const originalColor = originalBackground?.getStyle?.().color ?? math_1.Color4.transparent;
|
1109
|
+
const originalFillsScreen = this.image.getAutoresizeEnabled();
|
1110
|
+
const defaultType = (style.color && originalType === BackgroundComponent_1.BackgroundType.None ? BackgroundComponent_1.BackgroundType.SolidColor : originalType);
|
1111
|
+
const backgroundType = style.type ?? defaultType;
|
1112
|
+
const backgroundColor = style.color ?? originalColor;
|
1113
|
+
const fillsScreen = style.autoresize ?? originalFillsScreen;
|
1114
|
+
if (backgroundType !== BackgroundComponent_1.BackgroundType.None) {
|
1115
|
+
const newBackground = new BackgroundComponent_1.default(backgroundType, backgroundColor);
|
1116
|
+
commands.push(EditorImage_1.default.addElement(newBackground));
|
1117
|
+
}
|
1118
|
+
if (fillsScreen !== originalFillsScreen) {
|
1119
|
+
commands.push(this.image.setAutoresizeEnabled(fillsScreen));
|
1120
|
+
// Avoid 0x0 backgrounds
|
1121
|
+
if (!fillsScreen && this.image.getImportExportRect().maxDimension === 0) {
|
1122
|
+
commands.push(this.image.setImportExportRect(this.image.getImportExportRect().resizedTo(math_1.Vec2.of(500, 500))));
|
1123
|
+
}
|
1124
|
+
}
|
1125
|
+
return (0, uniteCommands_1.default)(commands);
|
1126
|
+
}
|
1129
1127
|
/**
|
1130
1128
|
* Set the background color of the image.
|
1129
|
+
*
|
1130
|
+
* This is a convenience method for adding or updating the {@link BackgroundComponent}
|
1131
|
+
* for the current image.
|
1132
|
+
*
|
1133
|
+
* @see {@link setBackgroundStyle}
|
1131
1134
|
*/
|
1132
1135
|
setBackgroundColor(color) {
|
1133
1136
|
let background = this.getTopmostBackgroundComponent();
|
package/dist/cjs/Pointer.d.ts
CHANGED
@@ -35,5 +35,6 @@ export default class Pointer {
|
|
35
35
|
*/
|
36
36
|
withCanvasPosition(canvasPos: Point2, viewport: Viewport): Pointer;
|
37
37
|
static ofEvent(evt: PointerEvent, isDown: boolean, viewport: Viewport, relativeTo?: HTMLElement): Pointer;
|
38
|
-
static ofCanvasPoint(canvasPos: Point2, isDown: boolean, viewport: Viewport, id?: number, device?: PointerDevice, isPrimary?: boolean, pressure?: number | null): Pointer;
|
38
|
+
static ofCanvasPoint(canvasPos: Point2, isDown: boolean, viewport: Viewport, id?: number, device?: PointerDevice, isPrimary?: boolean, pressure?: number | null, timeStamp?: number | null): Pointer;
|
39
|
+
static ofScreenPoint(screenPos: Point2, isDown: boolean, viewport: Viewport, id?: number, device?: PointerDevice, isPrimary?: boolean, pressure?: number | null, timeStamp?: number | null): Pointer;
|
39
40
|
}
|
package/dist/cjs/Pointer.js
CHANGED
@@ -113,9 +113,16 @@ class Pointer {
|
|
113
113
|
}
|
114
114
|
// Create a new Pointer from a point on the canvas.
|
115
115
|
// Intended for unit tests.
|
116
|
-
static ofCanvasPoint(canvasPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null) {
|
116
|
+
static ofCanvasPoint(canvasPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null, timeStamp = null) {
|
117
117
|
const screenPos = viewport.canvasToScreen(canvasPos);
|
118
|
-
|
118
|
+
timeStamp ??= performance.now();
|
119
|
+
return new Pointer(screenPos, canvasPos, pressure, isPrimary, isDown, device, id, timeStamp);
|
120
|
+
}
|
121
|
+
// Create a new Pointer from a point on the screen.
|
122
|
+
// Intended for unit tests.
|
123
|
+
static ofScreenPoint(screenPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null, timeStamp = null) {
|
124
|
+
const canvasPos = viewport.screenToCanvas(screenPos);
|
125
|
+
timeStamp ??= performance.now();
|
119
126
|
return new Pointer(screenPos, canvasPos, pressure, isPrimary, isDown, device, id, timeStamp);
|
120
127
|
}
|
121
128
|
}
|
@@ -20,6 +20,7 @@ export interface CommandLocalization {
|
|
20
20
|
duplicateAction: (elemDescription: string, count: number) => string;
|
21
21
|
inverseOf: (actionDescription: string) => string;
|
22
22
|
unionOf: (actionDescription: string, actionCount: number) => string;
|
23
|
+
andNMoreCommands: (count: number) => string;
|
23
24
|
selectedElements: (count: number) => string;
|
24
25
|
}
|
25
26
|
export declare const defaultCommandLocalization: CommandLocalization;
|
@@ -22,5 +22,6 @@ exports.defaultCommandLocalization = {
|
|
22
22
|
movedRight: 'Moved right',
|
23
23
|
zoomedOut: 'Zoomed out',
|
24
24
|
zoomedIn: 'Zoomed in',
|
25
|
+
andNMoreCommands: (count) => `And ${count} more commands.`,
|
25
26
|
selectedElements: (count) => `Selected ${count} element${count === 1 ? '' : 's'}`,
|
26
27
|
};
|
@@ -1,5 +1,9 @@
|
|
1
1
|
import Command from './Command';
|
2
2
|
import SerializableCommand from './SerializableCommand';
|
3
|
+
export interface UniteCommandsOptions {
|
4
|
+
applyChunkSize?: number;
|
5
|
+
description?: string;
|
6
|
+
}
|
3
7
|
/**
|
4
8
|
* Creates a single command from `commands`. This is useful when undoing should undo *all* commands
|
5
9
|
* in `commands` at once, rather than one at a time.
|
@@ -36,5 +40,5 @@ import SerializableCommand from './SerializableCommand';
|
|
36
40
|
* // applying them shouldn't be done all at once (which would block the UI).
|
37
41
|
* ```
|
38
42
|
*/
|
39
|
-
declare const uniteCommands: <T extends Command>(commands: T[],
|
43
|
+
declare const uniteCommands: <T extends Command>(commands: T[], options?: UniteCommandsOptions | number) => T extends SerializableCommand ? SerializableCommand : Command;
|
40
44
|
export default uniteCommands;
|
@@ -7,10 +7,11 @@ const waitForAll_1 = __importDefault(require("../util/waitForAll"));
|
|
7
7
|
const Command_1 = __importDefault(require("./Command"));
|
8
8
|
const SerializableCommand_1 = __importDefault(require("./SerializableCommand"));
|
9
9
|
class NonSerializableUnion extends Command_1.default {
|
10
|
-
constructor(commands, applyChunkSize) {
|
10
|
+
constructor(commands, applyChunkSize, descriptionOverride) {
|
11
11
|
super();
|
12
12
|
this.commands = commands;
|
13
13
|
this.applyChunkSize = applyChunkSize;
|
14
|
+
this.descriptionOverride = descriptionOverride;
|
14
15
|
}
|
15
16
|
apply(editor) {
|
16
17
|
if (this.applyChunkSize === undefined) {
|
@@ -36,9 +37,13 @@ class NonSerializableUnion extends Command_1.default {
|
|
36
37
|
this.commands.forEach(command => command.onDrop(editor));
|
37
38
|
}
|
38
39
|
description(editor, localizationTable) {
|
40
|
+
if (this.descriptionOverride) {
|
41
|
+
return this.descriptionOverride;
|
42
|
+
}
|
39
43
|
const descriptions = [];
|
40
44
|
let lastDescription = null;
|
41
45
|
let duplicateDescriptionCount = 0;
|
46
|
+
let handledCommandCount = 0;
|
42
47
|
for (const part of this.commands) {
|
43
48
|
const description = part.description(editor, localizationTable);
|
44
49
|
if (description !== lastDescription && lastDescription !== null) {
|
@@ -47,7 +52,13 @@ class NonSerializableUnion extends Command_1.default {
|
|
47
52
|
duplicateDescriptionCount = 0;
|
48
53
|
}
|
49
54
|
duplicateDescriptionCount++;
|
55
|
+
handledCommandCount++;
|
50
56
|
lastDescription ??= description;
|
57
|
+
// Long descriptions aren't very useful to the user.
|
58
|
+
const maxDescriptionLength = 12;
|
59
|
+
if (descriptions.length > maxDescriptionLength) {
|
60
|
+
break;
|
61
|
+
}
|
51
62
|
}
|
52
63
|
if (duplicateDescriptionCount > 1) {
|
53
64
|
descriptions.push(localizationTable.unionOf(lastDescription, duplicateDescriptionCount));
|
@@ -55,15 +66,19 @@ class NonSerializableUnion extends Command_1.default {
|
|
55
66
|
else if (duplicateDescriptionCount === 1) {
|
56
67
|
descriptions.push(lastDescription);
|
57
68
|
}
|
69
|
+
if (handledCommandCount < this.commands.length) {
|
70
|
+
descriptions.push(localizationTable.andNMoreCommands(this.commands.length - handledCommandCount));
|
71
|
+
}
|
58
72
|
return descriptions.join(', ');
|
59
73
|
}
|
60
74
|
}
|
61
75
|
class SerializableUnion extends SerializableCommand_1.default {
|
62
|
-
constructor(commands, applyChunkSize) {
|
76
|
+
constructor(commands, applyChunkSize, descriptionOverride) {
|
63
77
|
super('union');
|
64
78
|
this.commands = commands;
|
65
79
|
this.applyChunkSize = applyChunkSize;
|
66
|
-
this.
|
80
|
+
this.descriptionOverride = descriptionOverride;
|
81
|
+
this.nonserializableCommand = new NonSerializableUnion(commands, applyChunkSize, descriptionOverride);
|
67
82
|
}
|
68
83
|
serializeToJSON() {
|
69
84
|
if (this.serializedData) {
|
@@ -72,6 +87,7 @@ class SerializableUnion extends SerializableCommand_1.default {
|
|
72
87
|
return {
|
73
88
|
applyChunkSize: this.applyChunkSize,
|
74
89
|
data: this.commands.map(command => command.serialize()),
|
90
|
+
description: this.descriptionOverride,
|
75
91
|
};
|
76
92
|
}
|
77
93
|
apply(editor) {
|
@@ -125,7 +141,7 @@ class SerializableUnion extends SerializableCommand_1.default {
|
|
125
141
|
* // applying them shouldn't be done all at once (which would block the UI).
|
126
142
|
* ```
|
127
143
|
*/
|
128
|
-
const uniteCommands = (commands,
|
144
|
+
const uniteCommands = (commands, options) => {
|
129
145
|
let allSerializable = true;
|
130
146
|
for (const command of commands) {
|
131
147
|
if (!(command instanceof SerializableCommand_1.default)) {
|
@@ -133,12 +149,21 @@ const uniteCommands = (commands, applyChunkSize) => {
|
|
133
149
|
break;
|
134
150
|
}
|
135
151
|
}
|
152
|
+
let applyChunkSize;
|
153
|
+
let description;
|
154
|
+
if (typeof options === 'number') {
|
155
|
+
applyChunkSize = options;
|
156
|
+
}
|
157
|
+
else {
|
158
|
+
applyChunkSize = options?.applyChunkSize;
|
159
|
+
description = options?.description;
|
160
|
+
}
|
136
161
|
if (!allSerializable) {
|
137
|
-
return new NonSerializableUnion(commands, applyChunkSize);
|
162
|
+
return new NonSerializableUnion(commands, applyChunkSize, description);
|
138
163
|
}
|
139
164
|
else {
|
140
165
|
const castedCommands = commands;
|
141
|
-
return new SerializableUnion(castedCommands, applyChunkSize);
|
166
|
+
return new SerializableUnion(castedCommands, applyChunkSize, description);
|
142
167
|
}
|
143
168
|
};
|
144
169
|
SerializableCommand_1.default.register('union', (data, editor) => {
|
@@ -149,10 +174,11 @@ SerializableCommand_1.default.register('union', (data, editor) => {
|
|
149
174
|
if (typeof applyChunkSize !== 'number' && applyChunkSize !== undefined) {
|
150
175
|
throw new Error('serialized applyChunkSize is neither undefined nor a number.');
|
151
176
|
}
|
177
|
+
const description = typeof data.description === 'string' ? data.description : undefined;
|
152
178
|
const commands = [];
|
153
179
|
for (const part of data.data) {
|
154
180
|
commands.push(SerializableCommand_1.default.deserialize(part, editor));
|
155
181
|
}
|
156
|
-
return uniteCommands(commands, applyChunkSize);
|
182
|
+
return uniteCommands(commands, { applyChunkSize, description });
|
157
183
|
});
|
158
184
|
exports.default = uniteCommands;
|
@@ -1,8 +1,9 @@
|
|
1
1
|
import SerializableCommand from '../commands/SerializableCommand';
|
2
2
|
import EditorImage from '../image/EditorImage';
|
3
|
-
import { LineSegment2, Mat33, Rect2 } from '@js-draw/math';
|
3
|
+
import { LineSegment2, Mat33, Path, Rect2 } from '@js-draw/math';
|
4
4
|
import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
|
5
5
|
import { ImageComponentLocalization } from './localization';
|
6
|
+
import Viewport from '../Viewport';
|
6
7
|
export type LoadSaveData = (string[] | Record<symbol, string | number>);
|
7
8
|
export type LoadSaveDataTable = Record<string, Array<LoadSaveData>>;
|
8
9
|
export type DeserializeCallback = (data: string) => AbstractComponent;
|
@@ -112,7 +113,9 @@ export default abstract class AbstractComponent {
|
|
112
113
|
* this function.
|
113
114
|
*/
|
114
115
|
intersectsRect(rect: Rect2): boolean;
|
115
|
-
|
116
|
+
isSelectable(): boolean;
|
117
|
+
isBackground(): boolean;
|
118
|
+
getProportionalRenderingTime(): number;
|
116
119
|
protected abstract applyTransformation(affineTransfm: Mat33): void;
|
117
120
|
/**
|
118
121
|
* Returns a command that, when applied, transforms this by [affineTransfm] and
|
@@ -131,9 +134,6 @@ export default abstract class AbstractComponent {
|
|
131
134
|
* this command.
|
132
135
|
*/
|
133
136
|
setZIndexAndTransformBy(affineTransfm: Mat33, newZIndex: number, originalZIndex?: number): SerializableCommand;
|
134
|
-
isSelectable(): boolean;
|
135
|
-
isBackground(): boolean;
|
136
|
-
getProportionalRenderingTime(): number;
|
137
137
|
private static transformElementCommandId;
|
138
138
|
private static TransformElementCommand;
|
139
139
|
/**
|
@@ -143,6 +143,18 @@ export default abstract class AbstractComponent {
|
|
143
143
|
abstract description(localizationTable: ImageComponentLocalization): string;
|
144
144
|
protected abstract createClone(): AbstractComponent;
|
145
145
|
clone(): AbstractComponent;
|
146
|
+
/**
|
147
|
+
* **Optional method**: Divides this component into sections roughly along the given path,
|
148
|
+
* removing parts that are roughly within `shape`.
|
149
|
+
*
|
150
|
+
* **Notes**:
|
151
|
+
* - A default implementation may be provided for this method in the future. Until then,
|
152
|
+
* this method is `undefined` if unsupported.
|
153
|
+
*
|
154
|
+
* `viewport` should be provided to determine how newly-added points should be rounded.
|
155
|
+
*/
|
156
|
+
withRegionErased?(shape: Path, viewport: Viewport): AbstractComponent[];
|
157
|
+
protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
|
146
158
|
serialize(): {
|
147
159
|
name: string;
|
148
160
|
zIndex: number;
|
@@ -142,6 +142,21 @@ class AbstractComponent {
|
|
142
142
|
const testLines = rect.getEdges();
|
143
143
|
return testLines.some(edge => this.intersects(edge));
|
144
144
|
}
|
145
|
+
// @returns true iff this component can be selected (e.g. by the selection tool.)
|
146
|
+
isSelectable() {
|
147
|
+
return true;
|
148
|
+
}
|
149
|
+
// @returns true iff this component should be added to the background, rather than the
|
150
|
+
// foreground of the image.
|
151
|
+
isBackground() {
|
152
|
+
return false;
|
153
|
+
}
|
154
|
+
// @returns an approximation of the proportional time it takes to render this component.
|
155
|
+
// This is intended to be a rough estimate, but, for example, a stroke with two points sould have
|
156
|
+
// a renderingWeight approximately twice that of a stroke with one point.
|
157
|
+
getProportionalRenderingTime() {
|
158
|
+
return 1;
|
159
|
+
}
|
145
160
|
/**
|
146
161
|
* Returns a command that, when applied, transforms this by [affineTransfm] and
|
147
162
|
* updates the editor.
|
@@ -166,21 +181,6 @@ class AbstractComponent {
|
|
166
181
|
setZIndexAndTransformBy(affineTransfm, newZIndex, originalZIndex) {
|
167
182
|
return new AbstractComponent.TransformElementCommand(affineTransfm, this.getId(), this, newZIndex, originalZIndex);
|
168
183
|
}
|
169
|
-
// @returns true iff this component can be selected (e.g. by the selection tool.)
|
170
|
-
isSelectable() {
|
171
|
-
return true;
|
172
|
-
}
|
173
|
-
// @returns true iff this component should be added to the background, rather than the
|
174
|
-
// foreground of the image.
|
175
|
-
isBackground() {
|
176
|
-
return false;
|
177
|
-
}
|
178
|
-
// @returns an approximation of the proportional time it takes to render this component.
|
179
|
-
// This is intended to be a rough estimate, but, for example, a stroke with two points sould have
|
180
|
-
// a renderingWeight approximately twice that of a stroke with one point.
|
181
|
-
getProportionalRenderingTime() {
|
182
|
-
return 1;
|
183
|
-
}
|
184
184
|
// Returns a copy of this component.
|
185
185
|
clone() {
|
186
186
|
const clone = this.createClone();
|
@@ -6,6 +6,7 @@ import AbstractComponent from './AbstractComponent';
|
|
6
6
|
import { ImageComponentLocalization } from './localization';
|
7
7
|
import RestyleableComponent, { ComponentStyle } from './RestylableComponent';
|
8
8
|
import RenderablePathSpec, { RenderablePathSpecWithPath } from '../rendering/RenderablePathSpec';
|
9
|
+
import Viewport from '../Viewport';
|
9
10
|
/**
|
10
11
|
* Represents an {@link AbstractComponent} made up of one or more {@link Path}s.
|
11
12
|
*
|
@@ -46,10 +47,12 @@ export default class Stroke extends AbstractComponent implements RestyleableComp
|
|
46
47
|
* ]);
|
47
48
|
* ```
|
48
49
|
*/
|
49
|
-
constructor(parts: RenderablePathSpec[]);
|
50
|
+
constructor(parts: RenderablePathSpec[], initialZIndex?: number);
|
50
51
|
getStyle(): ComponentStyle;
|
51
52
|
updateStyle(style: ComponentStyle): SerializableCommand;
|
52
53
|
forceStyle(style: ComponentStyle, editor: Editor | null): void;
|
54
|
+
/** @beta -- May fail for concave `path`s */
|
55
|
+
withRegionErased(eraserPath: Path, viewport: Viewport): Stroke[];
|
53
56
|
intersects(line: LineSegment2): boolean;
|
54
57
|
intersectsRect(rect: Rect2): boolean;
|
55
58
|
private simplifiedPath;
|