js-draw 1.12.0 → 1.13.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/dist/Editor.css +66 -118
  2. package/dist/bundle.js +2 -2
  3. package/dist/bundledStyles.js +1 -1
  4. package/dist/cjs/Editor.d.ts +4 -2
  5. package/dist/cjs/Editor.js +50 -39
  6. package/dist/cjs/components/AbstractComponent.d.ts +7 -0
  7. package/dist/cjs/components/AbstractComponent.js +7 -5
  8. package/dist/cjs/components/util/StrokeSmoother.d.ts +0 -1
  9. package/dist/cjs/components/util/StrokeSmoother.js +6 -17
  10. package/dist/cjs/localizations/es.js +11 -22
  11. package/dist/cjs/rendering/Display.d.ts +2 -0
  12. package/dist/cjs/rendering/Display.js +4 -0
  13. package/dist/cjs/toolbar/DropdownToolbar.d.ts +3 -1
  14. package/dist/cjs/toolbar/DropdownToolbar.js +2 -0
  15. package/dist/cjs/toolbar/EdgeToolbar.js +6 -5
  16. package/dist/cjs/toolbar/widgets/BaseWidget.js +2 -0
  17. package/dist/cjs/toolbar/widgets/InsertImageWidget.js +11 -4
  18. package/dist/cjs/toolbar/widgets/components/makeGridSelector.js +10 -0
  19. package/dist/cjs/util/addLongPressOrHoverCssClasses.d.ts +10 -0
  20. package/dist/cjs/util/addLongPressOrHoverCssClasses.js +34 -0
  21. package/dist/cjs/util/fileToBase64Url.d.ts +9 -0
  22. package/dist/cjs/util/fileToBase64Url.js +39 -0
  23. package/dist/cjs/util/fileToBase64Url.test.d.ts +1 -0
  24. package/dist/cjs/util/listenForLongPressOrHover.d.ts +13 -0
  25. package/dist/cjs/util/listenForLongPressOrHover.js +80 -0
  26. package/dist/cjs/util/listenForLongPressOrHover.test.d.ts +1 -0
  27. package/dist/cjs/version.js +1 -1
  28. package/dist/mjs/Editor.d.ts +4 -2
  29. package/dist/mjs/Editor.mjs +50 -39
  30. package/dist/mjs/components/AbstractComponent.d.ts +7 -0
  31. package/dist/mjs/components/AbstractComponent.mjs +7 -5
  32. package/dist/mjs/components/util/StrokeSmoother.d.ts +0 -1
  33. package/dist/mjs/components/util/StrokeSmoother.mjs +6 -17
  34. package/dist/mjs/localizations/es.mjs +11 -22
  35. package/dist/mjs/rendering/Display.d.ts +2 -0
  36. package/dist/mjs/rendering/Display.mjs +4 -0
  37. package/dist/mjs/toolbar/DropdownToolbar.d.ts +3 -1
  38. package/dist/mjs/toolbar/DropdownToolbar.mjs +2 -0
  39. package/dist/mjs/toolbar/EdgeToolbar.mjs +6 -5
  40. package/dist/mjs/toolbar/widgets/BaseWidget.mjs +2 -0
  41. package/dist/mjs/toolbar/widgets/InsertImageWidget.mjs +11 -4
  42. package/dist/mjs/toolbar/widgets/components/makeGridSelector.mjs +10 -0
  43. package/dist/mjs/util/addLongPressOrHoverCssClasses.d.ts +10 -0
  44. package/dist/mjs/util/addLongPressOrHoverCssClasses.mjs +29 -0
  45. package/dist/mjs/util/fileToBase64Url.d.ts +9 -0
  46. package/dist/mjs/util/fileToBase64Url.mjs +37 -0
  47. package/dist/mjs/util/fileToBase64Url.test.d.ts +1 -0
  48. package/dist/mjs/util/listenForLongPressOrHover.d.ts +13 -0
  49. package/dist/mjs/util/listenForLongPressOrHover.mjs +78 -0
  50. package/dist/mjs/util/listenForLongPressOrHover.test.d.ts +1 -0
  51. package/dist/mjs/version.mjs +1 -1
  52. package/package.json +2 -2
  53. package/src/toolbar/EdgeToolbar.scss +19 -97
  54. package/src/toolbar/utils/labelVisibleOnHover.scss +114 -0
  55. package/src/toolbar/widgets/components/makeGridSelector.scss +1 -0
  56. package/dist/cjs/util/fileToBase64.d.ts +0 -3
  57. package/dist/cjs/util/fileToBase64.js +0 -15
  58. package/dist/mjs/util/fileToBase64.d.ts +0 -3
  59. package/dist/mjs/util/fileToBase64.mjs +0 -13
@@ -143,8 +143,10 @@ class EdgeToolbar extends AbstractToolbar_1.default {
143
143
  this.sidebarContainer.style.animation = `${animationProperties} ${constants_1.toolbarCSSPrefix}-edgemenu-transition-in`;
144
144
  this.menuContainer.style.animation = `${animationProperties} ${constants_1.toolbarCSSPrefix}-edgemenu-container-transition-in`;
145
145
  this.menuContainer.style.opacity = '1';
146
- // Focus the close button when first shown.
147
- this.closeButton.focus();
146
+ // Focus the close button when first shown, but prevent scroll because the button
147
+ // is likely at the bottom of the screen (and we want the full sidebar to remain
148
+ // visible).
149
+ this.closeButton.focus({ preventScroll: true, });
148
150
  }
149
151
  else {
150
152
  this.closeColorPickers();
@@ -272,9 +274,8 @@ class EdgeToolbar extends AbstractToolbar_1.default {
272
274
  if (event.target === this.menuContainer) {
273
275
  if (eventName === 'pointerdown') {
274
276
  this.sidebarVisible.set(false);
275
- }
276
- if (eventName === 'pointerup') {
277
- this.editor.focus();
277
+ // A delay seems necessary for the editor
278
+ setTimeout(() => this.editor.focus(), 0);
278
279
  }
279
280
  return true;
280
281
  }
@@ -20,6 +20,7 @@ const ToolbarShortcutHandler_1 = __importDefault(require("../../tools/ToolbarSho
20
20
  const inputEvents_1 = require("../../inputEvents");
21
21
  const constants_1 = require("../constants");
22
22
  const DropdownLayoutManager_1 = __importDefault(require("./layout/DropdownLayoutManager"));
23
+ const addLongPressOrHoverCssClasses_1 = __importDefault(require("../../util/addLongPressOrHoverCssClasses"));
23
24
  /**
24
25
  * A set of labels that allow toolbar themes to treat buttons differently.
25
26
  */
@@ -66,6 +67,7 @@ class BaseWidget {
66
67
  this.button.oncontextmenu = event => {
67
68
  event.preventDefault();
68
69
  };
70
+ (0, addLongPressOrHoverCssClasses_1.default)(this.button);
69
71
  }
70
72
  /**
71
73
  * Should return a constant true or false value. If true (the default),
@@ -9,7 +9,7 @@ const EditorImage_1 = __importDefault(require("../../image/EditorImage"));
9
9
  const uniteCommands_1 = __importDefault(require("../../commands/uniteCommands"));
10
10
  const SelectionTool_1 = __importDefault(require("../../tools/SelectionTool/SelectionTool"));
11
11
  const math_1 = require("@js-draw/math");
12
- const fileToBase64_1 = __importDefault(require("../../util/fileToBase64"));
12
+ const fileToBase64Url_1 = __importDefault(require("../../util/fileToBase64Url"));
13
13
  const BaseWidget_1 = __importDefault(require("./BaseWidget"));
14
14
  const types_1 = require("../../types");
15
15
  const constants_1 = require("../constants");
@@ -111,11 +111,13 @@ class InsertImageWidget extends BaseWidget_1.default {
111
111
  this.imagePreview.style.display = 'block';
112
112
  const image = files[0];
113
113
  let data = null;
114
+ let errorMessage = null;
114
115
  try {
115
- data = await (0, fileToBase64_1.default)(image);
116
+ data = await (0, fileToBase64Url_1.default)(image);
116
117
  }
117
- catch (e) {
118
- this.statusView.innerText = this.localizationTable.imageLoadError(e);
118
+ catch (error) {
119
+ console.error('Image load error', error);
120
+ errorMessage = this.localizationTable.imageLoadError(error);
119
121
  }
120
122
  if (data) {
121
123
  this.image = ImageWrapper.fromSrcAndPreview(data, this.imagePreview, () => this.onImageDataUpdate());
@@ -124,6 +126,11 @@ class InsertImageWidget extends BaseWidget_1.default {
124
126
  this.image = null;
125
127
  }
126
128
  this.onImageDataUpdate();
129
+ // Show the error after image update callbacks to ensure it is
130
+ // actually shown.
131
+ if (errorMessage) {
132
+ this.statusView.innerText = errorMessage;
133
+ }
127
134
  });
128
135
  altTextRow.replaceChildren(imageAltTextLabel, this.imageAltTextInput);
129
136
  actionButtonRow.replaceChildren(this.submitButton);
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  const ReactiveValue_1 = require("../../../util/ReactiveValue");
7
7
  const stopPropagationOfScrollingWheelEvents_1 = __importDefault(require("../../../util/stopPropagationOfScrollingWheelEvents"));
8
+ const addLongPressOrHoverCssClasses_1 = __importDefault(require("../../../util/addLongPressOrHoverCssClasses"));
8
9
  const constants_1 = require("../../constants");
9
10
  let idCounter = 0;
10
11
  /**
@@ -36,6 +37,9 @@ labelText, defaultId, choices) => {
36
37
  const button = document.createElement('input');
37
38
  button.type = 'radio';
38
39
  button.id = `${constants_1.toolbarCSSPrefix}-grid-select-button-${idCounter++}`;
40
+ // Some toolbars only show the label on hover. Having long press or hover
41
+ // CSS classes are helpful here.
42
+ (0, addLongPressOrHoverCssClasses_1.default)(buttonContainer);
39
43
  // Clicking any part of labelContainer triggers the radio button.
40
44
  const labelContainer = document.createElement('label');
41
45
  const rebuildLabel = () => {
@@ -81,6 +85,12 @@ labelText, defaultId, choices) => {
81
85
  button.onblur = () => {
82
86
  buttonContainer.classList.remove('focus-visible');
83
87
  };
88
+ // Prevent the right-click menu from being shown on long-press
89
+ // (important for some toolbars that use long-press gestures to
90
+ // show grid selector labels).
91
+ buttonContainer.oncontextmenu = event => {
92
+ event.preventDefault();
93
+ };
84
94
  buttonContainer.replaceChildren(button, labelContainer);
85
95
  menuContainer.appendChild(buttonContainer);
86
96
  // Set whether the current button is checked
@@ -0,0 +1,10 @@
1
+ /**
2
+ * When a pointer is inside `element`, after a delay, adds the `has-long-press-or-hover`
3
+ * CSS class to `element`.
4
+ *
5
+ * When no pointers are inside `element`, adds the CSS class `no-long-press-or-hover`.
6
+ */
7
+ declare const addLongPressOrHoverCssClasses: (element: HTMLElement) => {
8
+ removeEventListeners: () => void;
9
+ };
10
+ export default addLongPressOrHoverCssClasses;
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const listenForLongPressOrHover_1 = __importDefault(require("./listenForLongPressOrHover"));
7
+ /**
8
+ * When a pointer is inside `element`, after a delay, adds the `has-long-press-or-hover`
9
+ * CSS class to `element`.
10
+ *
11
+ * When no pointers are inside `element`, adds the CSS class `no-long-press-or-hover`.
12
+ */
13
+ const addLongPressOrHoverCssClasses = (element) => {
14
+ const hasLongPressClass = 'has-long-press-or-hover';
15
+ const noLongPressClass = 'no-long-press-or-hover';
16
+ element.classList.add('no-long-press-or-hover');
17
+ const { removeListeners } = (0, listenForLongPressOrHover_1.default)(element, {
18
+ onStart() {
19
+ element.classList.remove(noLongPressClass);
20
+ element.classList.add(hasLongPressClass);
21
+ },
22
+ onEnd() {
23
+ element.classList.add(noLongPressClass);
24
+ element.classList.remove(hasLongPressClass);
25
+ },
26
+ });
27
+ return {
28
+ removeEventListeners: () => {
29
+ element.classList.remove(noLongPressClass);
30
+ removeListeners();
31
+ },
32
+ };
33
+ };
34
+ exports.default = addLongPressOrHoverCssClasses;
@@ -0,0 +1,9 @@
1
+ export interface FileToBase64UrlOptions {
2
+ onprogress?: (evt: ProgressEvent<FileReader>) => void;
3
+ onWarning?: (message: string, error: any) => void;
4
+ }
5
+ /**
6
+ * Converts `file` to a base64 data URL.
7
+ */
8
+ declare const fileToBase64Url: (file: Blob, options?: FileToBase64UrlOptions) => Promise<string | null>;
9
+ export default fileToBase64Url;
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Converts `file` to a base64 data URL.
5
+ */
6
+ const fileToBase64Url = async (file, options = {}) => {
7
+ try {
8
+ const reader = new FileReader();
9
+ return await new Promise((resolve, reject) => {
10
+ reader.onload = () => resolve(reader.result);
11
+ reader.onerror = reject;
12
+ reader.onabort = reject;
13
+ reader.onprogress = (evt) => {
14
+ options.onprogress?.(evt);
15
+ };
16
+ reader.readAsDataURL(file);
17
+ });
18
+ }
19
+ catch (error) {
20
+ // Files can fail to load with a FileReader in some cases. For example,
21
+ // in iOS Lockdown mode, where FileReader is unavailable.
22
+ (options.onWarning ?? console.warn)('Unable to convert file to base64 with a FileReader: ', error);
23
+ const arrayBuffer = await file.arrayBuffer();
24
+ const array = new Uint8Array(arrayBuffer);
25
+ // step: must be divisible by 3 (3 bytes = 4 base64 numerals)
26
+ // If too large, this will fail (String.fromCharCode accepts a limited
27
+ // number of arguments).
28
+ const step = 30;
29
+ const result = [];
30
+ for (let i = 0; i < array.length; i += step) {
31
+ // btoa accepts only characters with byte value 0-255 (which can be created
32
+ // with String.fromCharCode)
33
+ const stringByteArray = String.fromCharCode(...array.slice(i, i + step));
34
+ result.push(btoa(stringByteArray));
35
+ }
36
+ return `data:${file.type ?? 'image/*'};base64,${result.join('')}`;
37
+ }
38
+ };
39
+ exports.default = fileToBase64Url;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ type Options = {
2
+ onStart: () => void;
3
+ onEnd: () => void;
4
+ longPressTimeout?: number;
5
+ };
6
+ /**
7
+ * Calls `options.onStart` at the start of a long press or hover.
8
+ * Calls `options.onEnd` when no pointers are within the container.
9
+ */
10
+ declare const listenForLongPressOrHover: (target: HTMLElement, options: Options) => {
11
+ removeListeners: () => void;
12
+ };
13
+ export default listenForLongPressOrHover;
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Calls `options.onStart` at the start of a long press or hover.
5
+ * Calls `options.onEnd` when no pointers are within the container.
6
+ */
7
+ const listenForLongPressOrHover = (target, options) => {
8
+ const pointersInside = new Map();
9
+ let timeoutId = null;
10
+ let isLongPressInProgress = false;
11
+ const updateTimeout = () => {
12
+ if (pointersInside.size === 0) {
13
+ if (isLongPressInProgress) {
14
+ isLongPressInProgress = false;
15
+ options.onEnd();
16
+ }
17
+ else if (timeoutId !== null) {
18
+ clearTimeout(timeoutId);
19
+ timeoutId = null;
20
+ }
21
+ }
22
+ else {
23
+ const nowTime = Date.now();
24
+ let timeSinceFirstPointer = 0; // ms
25
+ for (const record of pointersInside.values()) {
26
+ const timeSince = nowTime - record.timeEnter;
27
+ timeSinceFirstPointer = Math.max(timeSince, timeSinceFirstPointer);
28
+ }
29
+ const longPressTimeout = options.longPressTimeout ?? 700; // ms
30
+ if (timeoutId !== null) {
31
+ clearTimeout(timeoutId);
32
+ timeoutId = null;
33
+ }
34
+ const timeLeft = longPressTimeout - timeSinceFirstPointer;
35
+ if (timeLeft <= 0) {
36
+ options.onStart();
37
+ isLongPressInProgress = true;
38
+ }
39
+ else {
40
+ timeoutId = setTimeout(() => {
41
+ timeoutId = null;
42
+ updateTimeout();
43
+ }, timeLeft);
44
+ }
45
+ }
46
+ };
47
+ // Detects long press
48
+ const pointerEventListener = (event) => {
49
+ const eventRecord = {
50
+ timeEnter: Date.now(),
51
+ };
52
+ if (event.type === 'pointerenter') {
53
+ pointersInside.set(event.pointerId, eventRecord);
54
+ }
55
+ else if (event.type === 'pointerleave' || event.type === 'pointercancel') {
56
+ // In some cases (for example, a click with a stylus on Android/Chrome), moving the pen
57
+ // over the target, clicking, then moving the pen out of the target produces input
58
+ // similar to this:
59
+ // - pointerenter (pointerId: 4)
60
+ // - pointerleave (pointerId: 4)
61
+ // - pointerenter (pointerId: 6)
62
+ // - pointerenter (pointerId: 1)
63
+ // - pointerleave (pointerId: 6)
64
+ // Observe that no pointerleave event was fired for the pointer with ID 1.
65
+ pointersInside.clear();
66
+ }
67
+ updateTimeout();
68
+ };
69
+ target.addEventListener('pointerenter', pointerEventListener);
70
+ target.addEventListener('pointerleave', pointerEventListener);
71
+ target.addEventListener('pointercancel', pointerEventListener);
72
+ return {
73
+ removeListeners: () => {
74
+ target.removeEventListener('pointerenter', pointerEventListener);
75
+ target.removeEventListener('pointerleave', pointerEventListener);
76
+ target.removeEventListener('pointercancel', pointerEventListener);
77
+ },
78
+ };
79
+ };
80
+ exports.default = listenForLongPressOrHover;
@@ -0,0 +1 @@
1
+ export {};
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = {
4
- number: '1.12.0',
4
+ number: '1.13.2',
5
5
  };
@@ -253,6 +253,8 @@ export declare class Editor {
253
253
  addToolbar(defaultLayout?: boolean): AbstractToolbar;
254
254
  private registerListeners;
255
255
  private updateEditorSizeVariables;
256
+ /** @internal */
257
+ protected handleHTMLWheelEvent(event: WheelEvent): boolean | undefined;
256
258
  private pointers;
257
259
  private getPointerList;
258
260
  /**
@@ -312,9 +314,9 @@ export declare class Editor {
312
314
  remove: () => void;
313
315
  };
314
316
  /** @internal */
315
- protected handleHTMLKeyDownEvent(htmlEvent: KeyboardEvent): void;
317
+ protected handleHTMLKeyDownEvent(htmlEvent: KeyboardEvent): boolean;
316
318
  /** @internal */
317
- protected handleHTMLKeyUpEvent(htmlEvent: KeyboardEvent): void;
319
+ protected handleHTMLKeyUpEvent(htmlEvent: KeyboardEvent): boolean;
318
320
  /**
319
321
  * Adds event listners for keypresses (and drop events) on `elem` and forwards those
320
322
  * events to the editor.
@@ -13,7 +13,7 @@ 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 fileToBase64 from './util/fileToBase64.mjs';
16
+ import fileToBase64Url from './util/fileToBase64Url.mjs';
17
17
  import uniteCommands from './commands/uniteCommands.mjs';
18
18
  import SelectionTool from './tools/SelectionTool/SelectionTool.mjs';
19
19
  import Erase from './commands/Erase.mjs';
@@ -239,41 +239,7 @@ export class Editor {
239
239
  this.handleKeyEventsFrom(this.renderingRegion);
240
240
  this.handlePointerEventsFrom(this.accessibilityAnnounceArea);
241
241
  this.container.addEventListener('wheel', evt => {
242
- let delta = Vec3.of(evt.deltaX, evt.deltaY, evt.deltaZ);
243
- // Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
244
- // pinch-zooming.
245
- if (!evt.ctrlKey && !evt.metaKey) {
246
- if (!this.settings.wheelEventsEnabled) {
247
- return;
248
- }
249
- else if (this.settings.wheelEventsEnabled === 'only-if-focused') {
250
- const focusedChild = this.container.querySelector(':focus');
251
- if (!focusedChild) {
252
- return;
253
- }
254
- }
255
- }
256
- if (evt.deltaMode === WheelEvent.DOM_DELTA_LINE) {
257
- delta = delta.times(15);
258
- }
259
- else if (evt.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
260
- delta = delta.times(100);
261
- }
262
- if (evt.ctrlKey || evt.metaKey) {
263
- delta = Vec3.of(0, 0, evt.deltaY);
264
- }
265
- // Ensure that `pos` is relative to `this.renderingRegion`
266
- const bbox = this.renderingRegion.getBoundingClientRect();
267
- const pos = Vec2.of(evt.clientX, evt.clientY).minus(Vec2.of(bbox.left, bbox.top));
268
- if (this.toolController.dispatchInputEvent({
269
- kind: InputEvtType.WheelEvt,
270
- delta,
271
- screenPos: pos,
272
- })) {
273
- evt.preventDefault();
274
- return true;
275
- }
276
- return false;
242
+ this.handleHTMLWheelEvent(evt);
277
243
  });
278
244
  const handleResize = () => {
279
245
  this.viewport.updateScreenSize(Vec2.of(this.display.width, this.display.height));
@@ -317,6 +283,44 @@ export class Editor {
317
283
  this.container.style.setProperty('--editor-current-display-width-px', `${this.renderingRegion.clientWidth}px`);
318
284
  this.container.style.setProperty('--editor-current-display-height-px', `${this.renderingRegion.clientHeight}px`);
319
285
  }
286
+ /** @internal */
287
+ handleHTMLWheelEvent(event) {
288
+ let delta = Vec3.of(event.deltaX, event.deltaY, event.deltaZ);
289
+ // Process wheel events if the ctrl key is down, even if disabled -- we do want to handle
290
+ // pinch-zooming.
291
+ if (!event.ctrlKey && !event.metaKey) {
292
+ if (!this.settings.wheelEventsEnabled) {
293
+ return;
294
+ }
295
+ else if (this.settings.wheelEventsEnabled === 'only-if-focused') {
296
+ const focusedChild = this.container.querySelector(':focus');
297
+ if (!focusedChild) {
298
+ return;
299
+ }
300
+ }
301
+ }
302
+ if (event.deltaMode === WheelEvent.DOM_DELTA_LINE) {
303
+ delta = delta.times(15);
304
+ }
305
+ else if (event.deltaMode === WheelEvent.DOM_DELTA_PAGE) {
306
+ delta = delta.times(100);
307
+ }
308
+ if (event.ctrlKey || event.metaKey) {
309
+ delta = Vec3.of(0, 0, event.deltaY);
310
+ }
311
+ // Ensure that `pos` is relative to `this.renderingRegion`
312
+ const bbox = this.renderingRegion.getBoundingClientRect();
313
+ const pos = Vec2.of(event.clientX, event.clientY).minus(Vec2.of(bbox.left, bbox.top));
314
+ if (this.toolController.dispatchInputEvent({
315
+ kind: InputEvtType.WheelEvt,
316
+ delta,
317
+ screenPos: pos,
318
+ })) {
319
+ event.preventDefault();
320
+ return true;
321
+ }
322
+ return false;
323
+ }
320
324
  getPointerList() {
321
325
  const nowTime = performance.now();
322
326
  const res = [];
@@ -443,7 +447,7 @@ export class Editor {
443
447
  this.showLoadingWarning(evt.loaded / evt.total);
444
448
  };
445
449
  try {
446
- const data = await fileToBase64(file, onprogress);
450
+ const data = await fileToBase64Url(file, { onprogress });
447
451
  if (data && this.toolController.dispatchInputEvent({
448
452
  kind: InputEvtType.PasteEvent,
449
453
  mime: fileType,
@@ -625,14 +629,18 @@ export class Editor {
625
629
  const event = keyPressEventFromHTMLEvent(htmlEvent);
626
630
  if (this.toolController.dispatchInputEvent(event)) {
627
631
  htmlEvent.preventDefault();
632
+ return true;
628
633
  }
629
634
  else if (event.key === 't' || event.key === 'T') {
630
635
  htmlEvent.preventDefault();
631
636
  this.display.rerenderAsText();
637
+ return true;
632
638
  }
633
639
  else if (event.key === 'Escape') {
634
640
  this.renderingRegion.blur();
641
+ return true;
635
642
  }
643
+ return false;
636
644
  }
637
645
  /** @internal */
638
646
  handleHTMLKeyUpEvent(htmlEvent) {
@@ -640,7 +648,9 @@ export class Editor {
640
648
  const event = keyUpEventFromHTMLEvent(htmlEvent);
641
649
  if (this.toolController.dispatchInputEvent(event)) {
642
650
  htmlEvent.preventDefault();
651
+ return true;
643
652
  }
653
+ return false;
644
654
  }
645
655
  /**
646
656
  * Adds event listners for keypresses (and drop events) on `elem` and forwards those
@@ -1160,8 +1170,9 @@ export class Editor {
1160
1170
  ` ${this.viewport.getScaleFactor()}x zoom, ${180 / Math.PI * this.viewport.getRotationAngle()}° rotation`,
1161
1171
  ` ${this.image.estimateNumElements()} components`,
1162
1172
  ` auto-resize: ${this.image.getAutoresizeEnabled() ? 'enabled' : 'disabled'}`,
1163
- ` ${this.getImportExportRect().w}x${this.getImportExportRect().h} image size`,
1164
- ` ${screenSize.x}x${screenSize.y} screen size`,
1173
+ ` image size: ${this.getImportExportRect().w}x${this.getImportExportRect().h}`,
1174
+ ` screen size: ${screenSize.x}x${screenSize.y}`,
1175
+ ` device pixel ratio: ${this.display.getDevicePixelRatio()}`,
1165
1176
  ' cache:',
1166
1177
  ` ${this.display.getCache().getDebugInfo()
1167
1178
  // Indent
@@ -114,6 +114,13 @@ export default abstract class AbstractComponent {
114
114
  intersectsRect(rect: Rect2): boolean;
115
115
  protected abstract serializeToJSON(): any[] | Record<string, any> | number | string | null;
116
116
  protected abstract applyTransformation(affineTransfm: Mat33): void;
117
+ /**
118
+ * Returns a command that, when applied, transforms this by [affineTransfm] and
119
+ * updates the editor.
120
+ *
121
+ * The transformed component is also moved to the top (use
122
+ * {@link AbstractComponent.setZIndexAndTransformBy} to avoid this behavior).
123
+ */
117
124
  transformBy(affineTransfm: Mat33): SerializableCommand;
118
125
  setZIndex(newZIndex: number): SerializableCommand;
119
126
  /**
@@ -136,11 +136,13 @@ class AbstractComponent {
136
136
  const testLines = rect.getEdges();
137
137
  return testLines.some(edge => this.intersects(edge));
138
138
  }
139
- // Returns a command that, when applied, transforms this by [affineTransfm] and
140
- // updates the editor.
141
- //
142
- // The transformed component is also moved to the top (use {@link setZIndexAndTransformBy} to
143
- // avoid this behavior).
139
+ /**
140
+ * Returns a command that, when applied, transforms this by [affineTransfm] and
141
+ * updates the editor.
142
+ *
143
+ * The transformed component is also moved to the top (use
144
+ * {@link AbstractComponent.setZIndexAndTransformBy} to avoid this behavior).
145
+ */
144
146
  transformBy(affineTransfm) {
145
147
  return new AbstractComponent.TransformElementCommand(affineTransfm, this.getId(), this);
146
148
  }
@@ -15,7 +15,6 @@ export declare class StrokeSmoother {
15
15
  private onCurveAdded;
16
16
  private isFirstSegment;
17
17
  private buffer;
18
- private centerOfMass;
19
18
  private lastPoint;
20
19
  private lastExitingVec;
21
20
  private currentCurve;
@@ -12,7 +12,6 @@ export class StrokeSmoother {
12
12
  this.maxFitAllowed = maxFitAllowed;
13
13
  this.onCurveAdded = onCurveAdded;
14
14
  this.isFirstSegment = true;
15
- this.centerOfMass = null;
16
15
  this.lastExitingVec = null;
17
16
  this.currentCurve = null;
18
17
  this.lastPoint = this.startPoint;
@@ -58,7 +57,6 @@ export class StrokeSmoother {
58
57
  this.buffer[this.buffer.length - 2], lastPoint,
59
58
  ];
60
59
  this.currentCurve = null;
61
- this.centerOfMass = null;
62
60
  this.isFirstSegment = false;
63
61
  }
64
62
  // Returns [upper curve, connector, lower curve]
@@ -106,20 +104,10 @@ export class StrokeSmoother {
106
104
  if (shouldSnapToInitial) {
107
105
  return;
108
106
  }
109
- if (!this.centerOfMass) {
110
- this.centerOfMass = newPoint.pos;
111
- }
112
- else {
113
- this.centerOfMass = this.centerOfMass
114
- .times(this.buffer.length)
115
- .plus(newPoint.pos).times(1 / (this.buffer.length + 1));
116
- }
117
- const toCenterOfMass = this.centerOfMass.minus(newPoint.pos);
118
107
  const deltaTimeSeconds = deltaTime / 1000;
119
108
  const velocity = newPoint.pos.minus(this.lastPoint.pos).times(1 / deltaTimeSeconds);
120
109
  // TODO: Do we need momentum smoothing? (this.momentum.lerp(velocity, 0.9);)
121
- const k = 1;
122
- this.momentum = velocity.plus(toCenterOfMass.times(k));
110
+ this.momentum = velocity;
123
111
  }
124
112
  const lastPoint = this.lastPoint ?? newPoint;
125
113
  this.lastPoint = newPoint;
@@ -156,7 +144,7 @@ export class StrokeSmoother {
156
144
  }
157
145
  let exitingVec = this.computeExitingVec();
158
146
  // Find the intersection between the entering vector and the exiting vector
159
- const maxRelativeLength = 1.6;
147
+ const maxRelativeLength = 1.7;
160
148
  const segmentStart = this.buffer[0];
161
149
  const segmentEnd = newPoint.pos;
162
150
  const startEndDist = segmentEnd.minus(segmentStart).magnitude();
@@ -185,9 +173,10 @@ export class StrokeSmoother {
185
173
  .lerp(segmentStart.plus(enteringVec.times(startEndDist)), 0.25);
186
174
  }
187
175
  // Equal to an endpoint?
188
- if (segmentStart.eq(controlPoint) || segmentEnd.eq(controlPoint)) {
189
- // Position the control point between the two end points
190
- controlPoint = segmentStart.lerp(segmentEnd, 0.5);
176
+ else if (segmentStart.eq(controlPoint) || segmentEnd.eq(controlPoint)) {
177
+ // Position the control point closer to the first -- the connecting
178
+ // segment will be roughly a line.
179
+ controlPoint = segmentStart.plus(enteringVec.times(startEndDist / 5));
191
180
  }
192
181
  console.assert(!segmentStart.eq(controlPoint, 1e-11), 'Start and control points are equal!');
193
182
  console.assert(!controlPoint.eq(segmentEnd, 1e-11), 'Control and end points are equal!');