js-draw 1.16.1 → 1.17.0
Sign up to get free protection for your applications and to get access to all the features.
- package/dist/bundle.js +2 -2
- package/dist/bundledStyles.js +1 -1
- package/dist/cjs/Editor.d.ts +76 -6
- package/dist/cjs/Editor.js +89 -89
- 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/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 +115 -0
- package/dist/cjs/components/builders/PressureSensitiveFreehandLineBuilder.js +1 -1
- 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 +4 -1
- 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 +0 -2
- package/dist/cjs/localizations/es.js +2 -2
- 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/toolbar/IconProvider.d.ts +6 -3
- package/dist/cjs/toolbar/IconProvider.js +6 -4
- package/dist/cjs/toolbar/widgets/DocumentPropertiesWidget.js +24 -1
- package/dist/cjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
- package/dist/cjs/toolbar/widgets/PenToolWidget.js +7 -1
- package/dist/cjs/tools/Eraser.js +1 -1
- package/dist/cjs/tools/InputFilter/InputStabilizer.js +3 -3
- package/dist/cjs/tools/PasteHandler.js +36 -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/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 +76 -6
- package/dist/mjs/Editor.mjs +89 -89
- 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/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 +108 -0
- package/dist/mjs/components/builders/PressureSensitiveFreehandLineBuilder.mjs +1 -1
- 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 +4 -1
- 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 +0 -2
- package/dist/mjs/localizations/es.mjs +2 -2
- 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/toolbar/IconProvider.d.ts +6 -3
- package/dist/mjs/toolbar/IconProvider.mjs +6 -4
- package/dist/mjs/toolbar/widgets/DocumentPropertiesWidget.mjs +24 -1
- package/dist/mjs/toolbar/widgets/PenToolWidget.d.ts +1 -1
- package/dist/mjs/toolbar/widgets/PenToolWidget.mjs +7 -1
- package/dist/mjs/tools/Eraser.mjs +1 -1
- package/dist/mjs/tools/InputFilter/InputStabilizer.mjs +3 -3
- package/dist/mjs/tools/PasteHandler.mjs +36 -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/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/mjs/Editor.mjs
CHANGED
@@ -13,7 +13,6 @@ import getLocalizationTable from './localizations/getLocalizationTable.mjs';
|
|
13
13
|
import IconProvider from './toolbar/IconProvider.mjs';
|
14
14
|
import CanvasRenderer from './rendering/renderers/CanvasRenderer.mjs';
|
15
15
|
import untilNextAnimationFrame from './util/untilNextAnimationFrame.mjs';
|
16
|
-
import fileToBase64Url from './util/fileToBase64Url.mjs';
|
17
16
|
import uniteCommands from './commands/uniteCommands.mjs';
|
18
17
|
import SelectionTool from './tools/SelectionTool/SelectionTool.mjs';
|
19
18
|
import Erase from './commands/Erase.mjs';
|
@@ -29,6 +28,7 @@ import { editorImageToSVGSync, editorImageToSVGAsync } from './image/export/ed
|
|
29
28
|
import { MutableReactiveValue } from './util/ReactiveValue.mjs';
|
30
29
|
import listenForKeyboardEventsFrom from './util/listenForKeyboardEventsFrom.mjs';
|
31
30
|
import mitLicenseAttribution from './util/mitLicenseAttribution.mjs';
|
31
|
+
import ClipboardHandler from './util/ClipboardHandler.mjs';
|
32
32
|
/**
|
33
33
|
* The main entrypoint for the full editor.
|
34
34
|
*
|
@@ -109,6 +109,7 @@ export class Editor {
|
|
109
109
|
iconProvider: settings.iconProvider ?? new IconProvider(),
|
110
110
|
notices: [],
|
111
111
|
appInfo: settings.appInfo ? { ...settings.appInfo } : null,
|
112
|
+
pens: { additionalPenTypes: settings.pens?.additionalPenTypes ?? [], },
|
112
113
|
};
|
113
114
|
// Validate settings
|
114
115
|
if (this.settings.minZoom > this.settings.maxZoom) {
|
@@ -189,6 +190,16 @@ export class Editor {
|
|
189
190
|
}
|
190
191
|
});
|
191
192
|
}
|
193
|
+
/**
|
194
|
+
* @returns a shallow copy of the current settings of the editor.
|
195
|
+
*
|
196
|
+
* Do not modify.
|
197
|
+
*/
|
198
|
+
getCurrentSettings() {
|
199
|
+
return {
|
200
|
+
...this.settings,
|
201
|
+
};
|
202
|
+
}
|
192
203
|
/**
|
193
204
|
* @returns a reference to the editor's container.
|
194
205
|
*
|
@@ -275,19 +286,12 @@ export class Editor {
|
|
275
286
|
this.accessibilityControlArea.addEventListener('input', () => {
|
276
287
|
this.accessibilityControlArea.value = '';
|
277
288
|
});
|
278
|
-
|
289
|
+
const copyHandler = new ClipboardHandler(this);
|
290
|
+
document.addEventListener('copy', async (evt) => {
|
279
291
|
if (!this.isEventSink(document.querySelector(':focus'))) {
|
280
292
|
return;
|
281
293
|
}
|
282
|
-
|
283
|
-
if (this.toolController.dispatchInputEvent({
|
284
|
-
kind: InputEvtType.CopyEvent,
|
285
|
-
setData: (mime, data) => {
|
286
|
-
clipboardData?.setData(mime, data);
|
287
|
-
},
|
288
|
-
})) {
|
289
|
-
evt.preventDefault();
|
290
|
-
}
|
294
|
+
copyHandler.copy(evt);
|
291
295
|
});
|
292
296
|
document.addEventListener('paste', evt => {
|
293
297
|
this.handlePaste(evt);
|
@@ -355,11 +359,21 @@ export class Editor {
|
|
355
359
|
* (e.g. with synthetic events). @internal
|
356
360
|
*/
|
357
361
|
setPointerCapture(target, pointerId) {
|
358
|
-
|
362
|
+
try {
|
363
|
+
target.setPointerCapture(pointerId);
|
364
|
+
}
|
365
|
+
catch (error) {
|
366
|
+
console.warn('Failed to setPointerCapture', error);
|
367
|
+
}
|
359
368
|
}
|
360
369
|
/** Can be overridden in a testing environment to handle synthetic events. @internal */
|
361
370
|
releasePointerCapture(target, pointerId) {
|
362
|
-
|
371
|
+
try {
|
372
|
+
target.releasePointerCapture(pointerId);
|
373
|
+
}
|
374
|
+
catch (error) {
|
375
|
+
console.warn('Failed to releasePointerCapture', error);
|
376
|
+
}
|
363
377
|
}
|
364
378
|
/**
|
365
379
|
* Dispatches a `PointerEvent` to the editor. The target element for `evt` must have the same top left
|
@@ -385,7 +399,7 @@ export class Editor {
|
|
385
399
|
if (pointer.down) {
|
386
400
|
const prevData = this.pointers[pointer.id];
|
387
401
|
if (prevData) {
|
388
|
-
const distanceMoved = pointer.screenPos.
|
402
|
+
const distanceMoved = pointer.screenPos.distanceTo(prevData.screenPos);
|
389
403
|
// If the pointer moved less than two pixels, don't send a new event.
|
390
404
|
if (distanceMoved < 2) {
|
391
405
|
return false;
|
@@ -444,66 +458,7 @@ export class Editor {
|
|
444
458
|
if (!this.isEventSink(target)) {
|
445
459
|
return;
|
446
460
|
}
|
447
|
-
|
448
|
-
if (!clipboardData) {
|
449
|
-
return;
|
450
|
-
}
|
451
|
-
// Handle SVG files (prefer to PNG/JPEG)
|
452
|
-
for (const file of clipboardData.files) {
|
453
|
-
if (file.type.toLowerCase() === 'image/svg+xml') {
|
454
|
-
const text = await file.text();
|
455
|
-
if (this.toolController.dispatchInputEvent({
|
456
|
-
kind: InputEvtType.PasteEvent,
|
457
|
-
mime: file.type,
|
458
|
-
data: text,
|
459
|
-
})) {
|
460
|
-
evt.preventDefault();
|
461
|
-
return;
|
462
|
-
}
|
463
|
-
}
|
464
|
-
}
|
465
|
-
// Handle image files.
|
466
|
-
for (const file of clipboardData.files) {
|
467
|
-
const fileType = file.type.toLowerCase();
|
468
|
-
if (fileType === 'image/png' || fileType === 'image/jpg') {
|
469
|
-
this.showLoadingWarning(0);
|
470
|
-
const onprogress = (evt) => {
|
471
|
-
this.showLoadingWarning(evt.loaded / evt.total);
|
472
|
-
};
|
473
|
-
try {
|
474
|
-
const data = await fileToBase64Url(file, { onprogress });
|
475
|
-
if (data && this.toolController.dispatchInputEvent({
|
476
|
-
kind: InputEvtType.PasteEvent,
|
477
|
-
mime: fileType,
|
478
|
-
data: data,
|
479
|
-
})) {
|
480
|
-
evt.preventDefault();
|
481
|
-
this.hideLoadingWarning();
|
482
|
-
return;
|
483
|
-
}
|
484
|
-
}
|
485
|
-
catch (e) {
|
486
|
-
console.error('Error reading image:', e);
|
487
|
-
}
|
488
|
-
this.hideLoadingWarning();
|
489
|
-
}
|
490
|
-
}
|
491
|
-
// Supported MIMEs for text data, in order of preference
|
492
|
-
const supportedMIMEs = [
|
493
|
-
'image/svg+xml',
|
494
|
-
'text/plain',
|
495
|
-
];
|
496
|
-
for (const mime of supportedMIMEs) {
|
497
|
-
const data = clipboardData.getData(mime);
|
498
|
-
if (data && this.toolController.dispatchInputEvent({
|
499
|
-
kind: InputEvtType.PasteEvent,
|
500
|
-
mime,
|
501
|
-
data,
|
502
|
-
})) {
|
503
|
-
evt.preventDefault();
|
504
|
-
return;
|
505
|
-
}
|
506
|
-
}
|
461
|
+
return await new ClipboardHandler(this).paste(evt);
|
507
462
|
}
|
508
463
|
/**
|
509
464
|
* Forward pointer events from `elem` to this editor. Such that right-click/right-click drag
|
@@ -614,7 +569,7 @@ export class Editor {
|
|
614
569
|
const eventBuffer = gestureData[pointerId].eventBuffer;
|
615
570
|
// Skip if the pointer hasn't moved enough to not be a "click".
|
616
571
|
const strokeStartThreshold = 10;
|
617
|
-
const isWithinClickThreshold = gestureStartPos && currentPos.
|
572
|
+
const isWithinClickThreshold = gestureStartPos && currentPos.distanceTo(gestureStartPos) < strokeStartThreshold;
|
618
573
|
if (isWithinClickThreshold && !gestureData[pointerId].hasMovedSignificantly) {
|
619
574
|
eventBuffer.push([eventName, event]);
|
620
575
|
sendToEditor = false;
|
@@ -728,7 +683,8 @@ export class Editor {
|
|
728
683
|
/** `apply` a command. `command` will be announced for accessibility. */
|
729
684
|
dispatch(command, addToHistory = true) {
|
730
685
|
const dispatchResult = this.dispatchNoAnnounce(command, addToHistory);
|
731
|
-
|
686
|
+
const commandDescription = command.description(this, this.localization);
|
687
|
+
this.announceForAccessibility(commandDescription);
|
732
688
|
return dispatchResult;
|
733
689
|
}
|
734
690
|
/**
|
@@ -942,8 +898,11 @@ export class Editor {
|
|
942
898
|
* This is a convenience method that creates **and applies** a single command.
|
943
899
|
*
|
944
900
|
* If `selectComponents` is true (the default), the components are selected.
|
901
|
+
*
|
902
|
+
* `actionDescription`, if given, should be a screenreader-friendly description of the
|
903
|
+
* reason components were added (e.g. "pasted").
|
945
904
|
*/
|
946
|
-
async addAndCenterComponents(components, selectComponents = true) {
|
905
|
+
async addAndCenterComponents(components, selectComponents = true, actionDescription) {
|
947
906
|
let bbox = null;
|
948
907
|
for (const component of components) {
|
949
908
|
if (bbox) {
|
@@ -974,7 +933,7 @@ export class Editor {
|
|
974
933
|
commands.push(component.transformBy(transfm));
|
975
934
|
}
|
976
935
|
const applyChunkSize = 100;
|
977
|
-
await this.dispatch(uniteCommands(commands, applyChunkSize), true);
|
936
|
+
await this.dispatch(uniteCommands(commands, { applyChunkSize, description: actionDescription }), true);
|
978
937
|
if (selectComponents) {
|
979
938
|
for (const selectionTool of this.toolController.getMatchingTools(SelectionTool)) {
|
980
939
|
selectionTool.setEnabled(true);
|
@@ -994,17 +953,7 @@ export class Editor {
|
|
994
953
|
* [[include:doc-pages/inline-examples/adding-an-image-and-data-urls.md]]
|
995
954
|
*/
|
996
955
|
toDataURL(format = 'image/png', outputSize) {
|
997
|
-
const canvas =
|
998
|
-
const importExportViewport = this.image.getImportExportViewport();
|
999
|
-
const exportRectSize = importExportViewport.getScreenRectSize();
|
1000
|
-
const resolution = outputSize ?? exportRectSize;
|
1001
|
-
canvas.width = resolution.x;
|
1002
|
-
canvas.height = resolution.y;
|
1003
|
-
const ctx = canvas.getContext('2d');
|
1004
|
-
// Scale to ensure that the entire output is visible.
|
1005
|
-
const scaleFactor = Math.min(resolution.x / exportRectSize.x, resolution.y / exportRectSize.y);
|
1006
|
-
ctx.scale(scaleFactor, scaleFactor);
|
1007
|
-
const renderer = new CanvasRenderer(ctx, importExportViewport);
|
956
|
+
const { element: canvas, renderer } = CanvasRenderer.fromViewport(this.image.getImportExportViewport(), { canvasSize: outputSize });
|
1008
957
|
this.image.renderAll(renderer);
|
1009
958
|
const dataURL = canvas.toDataURL(format);
|
1010
959
|
return dataURL;
|
@@ -1097,8 +1046,59 @@ export class Editor {
|
|
1097
1046
|
}
|
1098
1047
|
return background;
|
1099
1048
|
}
|
1049
|
+
/**
|
1050
|
+
* This is a convenience method for adding or updating the {@link BackgroundComponent}
|
1051
|
+
* and {@link EditorImage.setAutoresizeEnabled} for the current image.
|
1052
|
+
*
|
1053
|
+
* If there are multiple {@link BackgroundComponent}s in the image, this only modifies
|
1054
|
+
* the topmost such element.
|
1055
|
+
*
|
1056
|
+
* **Example**:
|
1057
|
+
* ```ts,runnable
|
1058
|
+
* import { Editor, Color4, BackgroundComponentBackgroundType } from 'js-draw';
|
1059
|
+
* const editor = new Editor(document.body);
|
1060
|
+
* editor.dispatch(editor.setBackgroundStyle({
|
1061
|
+
* color: Color4.orange,
|
1062
|
+
* type: BackgroundComponentBackgroundType.Grid,
|
1063
|
+
* autoresize: true,
|
1064
|
+
* }));
|
1065
|
+
* ```
|
1066
|
+
*
|
1067
|
+
* To change the background size, see {@link EditorImage.setImportExportRect}.
|
1068
|
+
*/
|
1069
|
+
setBackgroundStyle(style) {
|
1070
|
+
const originalBackground = this.getTopmostBackgroundComponent();
|
1071
|
+
const commands = [];
|
1072
|
+
if (originalBackground) {
|
1073
|
+
commands.push(new Erase([originalBackground]));
|
1074
|
+
}
|
1075
|
+
const originalType = originalBackground?.getBackgroundType?.() ?? BackgroundType.None;
|
1076
|
+
const originalColor = originalBackground?.getStyle?.().color ?? Color4.transparent;
|
1077
|
+
const originalFillsScreen = this.image.getAutoresizeEnabled();
|
1078
|
+
const defaultType = (style.color && originalType === BackgroundType.None ? BackgroundType.SolidColor : originalType);
|
1079
|
+
const backgroundType = style.type ?? defaultType;
|
1080
|
+
const backgroundColor = style.color ?? originalColor;
|
1081
|
+
const fillsScreen = style.autoresize ?? originalFillsScreen;
|
1082
|
+
if (backgroundType !== BackgroundType.None) {
|
1083
|
+
const newBackground = new BackgroundComponent(backgroundType, backgroundColor);
|
1084
|
+
commands.push(EditorImage.addElement(newBackground));
|
1085
|
+
}
|
1086
|
+
if (fillsScreen !== originalFillsScreen) {
|
1087
|
+
commands.push(this.image.setAutoresizeEnabled(fillsScreen));
|
1088
|
+
// Avoid 0x0 backgrounds
|
1089
|
+
if (!fillsScreen && this.image.getImportExportRect().maxDimension === 0) {
|
1090
|
+
commands.push(this.image.setImportExportRect(this.image.getImportExportRect().resizedTo(Vec2.of(500, 500))));
|
1091
|
+
}
|
1092
|
+
}
|
1093
|
+
return uniteCommands(commands);
|
1094
|
+
}
|
1100
1095
|
/**
|
1101
1096
|
* Set the background color of the image.
|
1097
|
+
*
|
1098
|
+
* This is a convenience method for adding or updating the {@link BackgroundComponent}
|
1099
|
+
* for the current image.
|
1100
|
+
*
|
1101
|
+
* @see {@link setBackgroundStyle}
|
1102
1102
|
*/
|
1103
1103
|
setBackgroundColor(color) {
|
1104
1104
|
let background = this.getTopmostBackgroundComponent();
|
package/dist/mjs/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/mjs/Pointer.mjs
CHANGED
@@ -110,9 +110,16 @@ export default class Pointer {
|
|
110
110
|
}
|
111
111
|
// Create a new Pointer from a point on the canvas.
|
112
112
|
// Intended for unit tests.
|
113
|
-
static ofCanvasPoint(canvasPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null) {
|
113
|
+
static ofCanvasPoint(canvasPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null, timeStamp = null) {
|
114
114
|
const screenPos = viewport.canvasToScreen(canvasPos);
|
115
|
-
|
115
|
+
timeStamp ??= performance.now();
|
116
|
+
return new Pointer(screenPos, canvasPos, pressure, isPrimary, isDown, device, id, timeStamp);
|
117
|
+
}
|
118
|
+
// Create a new Pointer from a point on the screen.
|
119
|
+
// Intended for unit tests.
|
120
|
+
static ofScreenPoint(screenPos, isDown, viewport, id = 0, device = PointerDevice.Pen, isPrimary = true, pressure = null, timeStamp = null) {
|
121
|
+
const canvasPos = viewport.screenToCanvas(screenPos);
|
122
|
+
timeStamp ??= performance.now();
|
116
123
|
return new Pointer(screenPos, canvasPos, pressure, isPrimary, isDown, device, id, timeStamp);
|
117
124
|
}
|
118
125
|
}
|
@@ -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;
|
@@ -19,5 +19,6 @@ export const defaultCommandLocalization = {
|
|
19
19
|
movedRight: 'Moved right',
|
20
20
|
zoomedOut: 'Zoomed out',
|
21
21
|
zoomedIn: 'Zoomed in',
|
22
|
+
andNMoreCommands: (count) => `And ${count} more commands.`,
|
22
23
|
selectedElements: (count) => `Selected ${count} element${count === 1 ? '' : 's'}`,
|
23
24
|
};
|
@@ -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;
|
@@ -2,10 +2,11 @@ import waitForAll from '../util/waitForAll.mjs';
|
|
2
2
|
import Command from './Command.mjs';
|
3
3
|
import SerializableCommand from './SerializableCommand.mjs';
|
4
4
|
class NonSerializableUnion extends Command {
|
5
|
-
constructor(commands, applyChunkSize) {
|
5
|
+
constructor(commands, applyChunkSize, descriptionOverride) {
|
6
6
|
super();
|
7
7
|
this.commands = commands;
|
8
8
|
this.applyChunkSize = applyChunkSize;
|
9
|
+
this.descriptionOverride = descriptionOverride;
|
9
10
|
}
|
10
11
|
apply(editor) {
|
11
12
|
if (this.applyChunkSize === undefined) {
|
@@ -31,9 +32,13 @@ class NonSerializableUnion extends Command {
|
|
31
32
|
this.commands.forEach(command => command.onDrop(editor));
|
32
33
|
}
|
33
34
|
description(editor, localizationTable) {
|
35
|
+
if (this.descriptionOverride) {
|
36
|
+
return this.descriptionOverride;
|
37
|
+
}
|
34
38
|
const descriptions = [];
|
35
39
|
let lastDescription = null;
|
36
40
|
let duplicateDescriptionCount = 0;
|
41
|
+
let handledCommandCount = 0;
|
37
42
|
for (const part of this.commands) {
|
38
43
|
const description = part.description(editor, localizationTable);
|
39
44
|
if (description !== lastDescription && lastDescription !== null) {
|
@@ -42,7 +47,13 @@ class NonSerializableUnion extends Command {
|
|
42
47
|
duplicateDescriptionCount = 0;
|
43
48
|
}
|
44
49
|
duplicateDescriptionCount++;
|
50
|
+
handledCommandCount++;
|
45
51
|
lastDescription ??= description;
|
52
|
+
// Long descriptions aren't very useful to the user.
|
53
|
+
const maxDescriptionLength = 12;
|
54
|
+
if (descriptions.length > maxDescriptionLength) {
|
55
|
+
break;
|
56
|
+
}
|
46
57
|
}
|
47
58
|
if (duplicateDescriptionCount > 1) {
|
48
59
|
descriptions.push(localizationTable.unionOf(lastDescription, duplicateDescriptionCount));
|
@@ -50,15 +61,19 @@ class NonSerializableUnion extends Command {
|
|
50
61
|
else if (duplicateDescriptionCount === 1) {
|
51
62
|
descriptions.push(lastDescription);
|
52
63
|
}
|
64
|
+
if (handledCommandCount < this.commands.length) {
|
65
|
+
descriptions.push(localizationTable.andNMoreCommands(this.commands.length - handledCommandCount));
|
66
|
+
}
|
53
67
|
return descriptions.join(', ');
|
54
68
|
}
|
55
69
|
}
|
56
70
|
class SerializableUnion extends SerializableCommand {
|
57
|
-
constructor(commands, applyChunkSize) {
|
71
|
+
constructor(commands, applyChunkSize, descriptionOverride) {
|
58
72
|
super('union');
|
59
73
|
this.commands = commands;
|
60
74
|
this.applyChunkSize = applyChunkSize;
|
61
|
-
this.
|
75
|
+
this.descriptionOverride = descriptionOverride;
|
76
|
+
this.nonserializableCommand = new NonSerializableUnion(commands, applyChunkSize, descriptionOverride);
|
62
77
|
}
|
63
78
|
serializeToJSON() {
|
64
79
|
if (this.serializedData) {
|
@@ -67,6 +82,7 @@ class SerializableUnion extends SerializableCommand {
|
|
67
82
|
return {
|
68
83
|
applyChunkSize: this.applyChunkSize,
|
69
84
|
data: this.commands.map(command => command.serialize()),
|
85
|
+
description: this.descriptionOverride,
|
70
86
|
};
|
71
87
|
}
|
72
88
|
apply(editor) {
|
@@ -120,7 +136,7 @@ class SerializableUnion extends SerializableCommand {
|
|
120
136
|
* // applying them shouldn't be done all at once (which would block the UI).
|
121
137
|
* ```
|
122
138
|
*/
|
123
|
-
const uniteCommands = (commands,
|
139
|
+
const uniteCommands = (commands, options) => {
|
124
140
|
let allSerializable = true;
|
125
141
|
for (const command of commands) {
|
126
142
|
if (!(command instanceof SerializableCommand)) {
|
@@ -128,12 +144,21 @@ const uniteCommands = (commands, applyChunkSize) => {
|
|
128
144
|
break;
|
129
145
|
}
|
130
146
|
}
|
147
|
+
let applyChunkSize;
|
148
|
+
let description;
|
149
|
+
if (typeof options === 'number') {
|
150
|
+
applyChunkSize = options;
|
151
|
+
}
|
152
|
+
else {
|
153
|
+
applyChunkSize = options?.applyChunkSize;
|
154
|
+
description = options?.description;
|
155
|
+
}
|
131
156
|
if (!allSerializable) {
|
132
|
-
return new NonSerializableUnion(commands, applyChunkSize);
|
157
|
+
return new NonSerializableUnion(commands, applyChunkSize, description);
|
133
158
|
}
|
134
159
|
else {
|
135
160
|
const castedCommands = commands;
|
136
|
-
return new SerializableUnion(castedCommands, applyChunkSize);
|
161
|
+
return new SerializableUnion(castedCommands, applyChunkSize, description);
|
137
162
|
}
|
138
163
|
};
|
139
164
|
SerializableCommand.register('union', (data, editor) => {
|
@@ -144,10 +169,11 @@ SerializableCommand.register('union', (data, editor) => {
|
|
144
169
|
if (typeof applyChunkSize !== 'number' && applyChunkSize !== undefined) {
|
145
170
|
throw new Error('serialized applyChunkSize is neither undefined nor a number.');
|
146
171
|
}
|
172
|
+
const description = typeof data.description === 'string' ? data.description : undefined;
|
147
173
|
const commands = [];
|
148
174
|
for (const part of data.data) {
|
149
175
|
commands.push(SerializableCommand.deserialize(part, editor));
|
150
176
|
}
|
151
|
-
return uniteCommands(commands, applyChunkSize);
|
177
|
+
return uniteCommands(commands, { applyChunkSize, description });
|
152
178
|
});
|
153
179
|
export default uniteCommands;
|
@@ -19,6 +19,41 @@ export declare enum TextTransformMode {
|
|
19
19
|
type TextElement = TextComponent | string;
|
20
20
|
/**
|
21
21
|
* Displays text.
|
22
|
+
*
|
23
|
+
* A `TextComponent` is a collection of `TextElement`s (`string`s or {@link TextComponent}s).
|
24
|
+
*
|
25
|
+
* **Example**:
|
26
|
+
*
|
27
|
+
* ```ts,runnable
|
28
|
+
* import { Editor, TextComponent, Mat33, Vec2, Color4, TextRenderingStyle } from 'js-draw';
|
29
|
+
* const editor = new Editor(document.body);
|
30
|
+
* editor.dispatch(editor.setBackgroundStyle({ color: Color4.black, autoresize: true ));
|
31
|
+
* ---visible---
|
32
|
+
* /// Adding a simple TextComponent
|
33
|
+
* ///------------------------------
|
34
|
+
*
|
35
|
+
* const positioning1 = Mat33.translation(Vec2.of(10, 10));
|
36
|
+
* const style: TextRenderingStyle = {
|
37
|
+
* fontFamily: 'sans', size: 12, renderingStyle: { fill: Color4.green },
|
38
|
+
* };
|
39
|
+
*
|
40
|
+
* editor.dispatch(
|
41
|
+
* editor.image.addElement(new TextComponent(['Hello, world'], positioning1, style)),
|
42
|
+
* );
|
43
|
+
*
|
44
|
+
*
|
45
|
+
* /// Adding nested TextComponents
|
46
|
+
* ///-----------------------------
|
47
|
+
*
|
48
|
+
* // Add another TextComponent that contains text and a TextComponent. Observe that '[Test]'
|
49
|
+
* // is placed directly after 'Test'.
|
50
|
+
* const positioning2 = Mat33.translation(Vec2.of(10, 50));
|
51
|
+
* editor.dispatch(
|
52
|
+
* editor.image.addElement(
|
53
|
+
* new TextComponent([ new TextComponent(['Test'], positioning1, style), '[Test]' ], positioning2, style)
|
54
|
+
* ),
|
55
|
+
* );
|
56
|
+
* ```
|
22
57
|
*/
|
23
58
|
export default class TextComponent extends AbstractComponent implements RestyleableComponent {
|
24
59
|
protected readonly textObjects: Array<TextElement>;
|
@@ -32,7 +67,7 @@ export default class TextComponent extends AbstractComponent implements Restylea
|
|
32
67
|
*
|
33
68
|
* @see {@link fromLines}
|
34
69
|
*/
|
35
|
-
constructor(textObjects: Array<TextElement>, transform: Mat33, style
|
70
|
+
constructor(textObjects: Array<TextElement>, transform: Mat33, style?: TextRenderingStyle, transformMode?: TextTransformMode);
|
36
71
|
static applyTextStyles(ctx: CanvasRenderingContext2D, style: TextRenderingStyle): void;
|
37
72
|
private static textMeasuringCtx;
|
38
73
|
private static estimateTextDimens;
|
@@ -1,4 +1,4 @@
|
|
1
|
-
import { Vec2, Rect2, Mat33 } from '@js-draw/math';
|
1
|
+
import { Vec2, Rect2, Mat33, Color4 } from '@js-draw/math';
|
2
2
|
import { cloneTextStyle, textStyleFromJSON, textStyleToJSON } from '../rendering/TextRenderingStyle.mjs';
|
3
3
|
import AbstractComponent from './AbstractComponent.mjs';
|
4
4
|
import { createRestyleComponentCommand } from './RestylableComponent.mjs';
|
@@ -14,8 +14,46 @@ export var TextTransformMode;
|
|
14
14
|
/**Relatively positioned in the Y direction, absolutely positioned in the X direction. */
|
15
15
|
TextTransformMode[TextTransformMode["RELATIVE_Y_ABSOLUTE_X"] = 3] = "RELATIVE_Y_ABSOLUTE_X";
|
16
16
|
})(TextTransformMode || (TextTransformMode = {}));
|
17
|
+
const defaultTextStyle = {
|
18
|
+
fontFamily: 'sans', size: 12, renderingStyle: { fill: Color4.purple },
|
19
|
+
};
|
17
20
|
/**
|
18
21
|
* Displays text.
|
22
|
+
*
|
23
|
+
* A `TextComponent` is a collection of `TextElement`s (`string`s or {@link TextComponent}s).
|
24
|
+
*
|
25
|
+
* **Example**:
|
26
|
+
*
|
27
|
+
* ```ts,runnable
|
28
|
+
* import { Editor, TextComponent, Mat33, Vec2, Color4, TextRenderingStyle } from 'js-draw';
|
29
|
+
* const editor = new Editor(document.body);
|
30
|
+
* editor.dispatch(editor.setBackgroundStyle({ color: Color4.black, autoresize: true ));
|
31
|
+
* ---visible---
|
32
|
+
* /// Adding a simple TextComponent
|
33
|
+
* ///------------------------------
|
34
|
+
*
|
35
|
+
* const positioning1 = Mat33.translation(Vec2.of(10, 10));
|
36
|
+
* const style: TextRenderingStyle = {
|
37
|
+
* fontFamily: 'sans', size: 12, renderingStyle: { fill: Color4.green },
|
38
|
+
* };
|
39
|
+
*
|
40
|
+
* editor.dispatch(
|
41
|
+
* editor.image.addElement(new TextComponent(['Hello, world'], positioning1, style)),
|
42
|
+
* );
|
43
|
+
*
|
44
|
+
*
|
45
|
+
* /// Adding nested TextComponents
|
46
|
+
* ///-----------------------------
|
47
|
+
*
|
48
|
+
* // Add another TextComponent that contains text and a TextComponent. Observe that '[Test]'
|
49
|
+
* // is placed directly after 'Test'.
|
50
|
+
* const positioning2 = Mat33.translation(Vec2.of(10, 50));
|
51
|
+
* editor.dispatch(
|
52
|
+
* editor.image.addElement(
|
53
|
+
* new TextComponent([ new TextComponent(['Test'], positioning1, style), '[Test]' ], positioning2, style)
|
54
|
+
* ),
|
55
|
+
* );
|
56
|
+
* ```
|
19
57
|
*/
|
20
58
|
class TextComponent extends AbstractComponent {
|
21
59
|
/**
|
@@ -25,7 +63,7 @@ class TextComponent extends AbstractComponent {
|
|
25
63
|
*/
|
26
64
|
constructor(textObjects,
|
27
65
|
// Transformation relative to this component's parent element.
|
28
|
-
transform, style,
|
66
|
+
transform, style = defaultTextStyle,
|
29
67
|
// @internal
|
30
68
|
transformMode = TextTransformMode.ABSOLUTE_XY) {
|
31
69
|
super(componentTypeId);
|
@@ -21,7 +21,7 @@ export default class ArrowBuilder {
|
|
21
21
|
const lineStartPoint = this.startPoint.pos;
|
22
22
|
const endPoint = this.endPoint.pos;
|
23
23
|
const toEnd = endPoint.minus(lineStartPoint).normalized();
|
24
|
-
const arrowLength = endPoint.
|
24
|
+
const arrowLength = endPoint.distanceTo(lineStartPoint);
|
25
25
|
// Ensure that the arrow tip is smaller than the arrow.
|
26
26
|
const arrowTipSize = Math.min(this.getLineWidth(), arrowLength / 2);
|
27
27
|
const startSize = this.startPoint.width / 2;
|
@@ -0,0 +1,35 @@
|
|
1
|
+
import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
|
2
|
+
import RenderablePathSpec from '../../rendering/RenderablePathSpec';
|
3
|
+
import { Rect2 } from '@js-draw/math';
|
4
|
+
import Stroke from '../Stroke';
|
5
|
+
import Viewport from '../../Viewport';
|
6
|
+
import { StrokeDataPoint } from '../../types';
|
7
|
+
import { ComponentBuilder, ComponentBuilderFactory } from './types';
|
8
|
+
import RenderingStyle from '../../rendering/RenderingStyle';
|
9
|
+
/**
|
10
|
+
* Creates strokes from line segments rather than Bézier curves.
|
11
|
+
*
|
12
|
+
* @beta Output behavior may change significantly between versions. For now, intended for debugging.
|
13
|
+
*/
|
14
|
+
export declare const makePolylineBuilder: ComponentBuilderFactory;
|
15
|
+
export default class PolylineBuilder implements ComponentBuilder {
|
16
|
+
private minFitAllowed;
|
17
|
+
private viewport;
|
18
|
+
private parts;
|
19
|
+
private bbox;
|
20
|
+
private averageWidth;
|
21
|
+
private widthAverageNumSamples;
|
22
|
+
private lastPoint;
|
23
|
+
private startPoint;
|
24
|
+
constructor(startPoint: StrokeDataPoint, minFitAllowed: number, viewport: Viewport);
|
25
|
+
getBBox(): Rect2;
|
26
|
+
protected getRenderingStyle(): RenderingStyle;
|
27
|
+
protected previewCurrentPath(): RenderablePathSpec;
|
28
|
+
protected previewFullPath(): RenderablePathSpec[];
|
29
|
+
preview(renderer: AbstractRenderer): void;
|
30
|
+
build(): Stroke;
|
31
|
+
private getMinFit;
|
32
|
+
private roundPoint;
|
33
|
+
private roundDistance;
|
34
|
+
addPoint(newPoint: StrokeDataPoint): void;
|
35
|
+
}
|