js-draw 1.6.1 → 1.7.0

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 (60) hide show
  1. package/README.md +0 -2
  2. package/dist/Editor.css +30 -4
  3. package/dist/bundle.js +2 -2
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/Editor.d.ts +5 -0
  6. package/dist/cjs/Editor.js +53 -70
  7. package/dist/cjs/components/BackgroundComponent.js +6 -1
  8. package/dist/cjs/components/TextComponent.d.ts +1 -1
  9. package/dist/cjs/components/TextComponent.js +19 -12
  10. package/dist/cjs/localization.d.ts +2 -0
  11. package/dist/cjs/localization.js +2 -0
  12. package/dist/cjs/localizations/comments.js +1 -0
  13. package/dist/cjs/rendering/RenderablePathSpec.js +16 -1
  14. package/dist/cjs/rendering/caching/CacheRecordManager.d.ts +1 -0
  15. package/dist/cjs/rendering/caching/CacheRecordManager.js +18 -0
  16. package/dist/cjs/rendering/caching/RenderingCache.d.ts +1 -0
  17. package/dist/cjs/rendering/caching/RenderingCache.js +3 -0
  18. package/dist/cjs/rendering/renderers/CanvasRenderer.js +3 -2
  19. package/dist/cjs/tools/SelectionTool/Selection.d.ts +5 -4
  20. package/dist/cjs/tools/SelectionTool/Selection.js +75 -48
  21. package/dist/cjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
  22. package/dist/cjs/tools/SelectionTool/SelectionHandle.js +8 -3
  23. package/dist/cjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
  24. package/dist/cjs/tools/SelectionTool/SelectionTool.js +36 -16
  25. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  26. package/dist/cjs/tools/SelectionTool/ToPointerAutoscroller.js +83 -0
  27. package/dist/cjs/tools/SelectionTool/TransformMode.d.ts +10 -3
  28. package/dist/cjs/tools/SelectionTool/TransformMode.js +52 -9
  29. package/dist/cjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
  30. package/dist/cjs/util/listenForKeyboardEventsFrom.js +142 -0
  31. package/dist/cjs/version.js +1 -1
  32. package/dist/mjs/Editor.d.ts +5 -0
  33. package/dist/mjs/Editor.mjs +53 -70
  34. package/dist/mjs/components/BackgroundComponent.mjs +6 -1
  35. package/dist/mjs/components/TextComponent.d.ts +1 -1
  36. package/dist/mjs/components/TextComponent.mjs +19 -12
  37. package/dist/mjs/localization.d.ts +2 -0
  38. package/dist/mjs/localization.mjs +2 -0
  39. package/dist/mjs/localizations/comments.mjs +1 -0
  40. package/dist/mjs/rendering/RenderablePathSpec.mjs +16 -1
  41. package/dist/mjs/rendering/caching/CacheRecordManager.d.ts +1 -0
  42. package/dist/mjs/rendering/caching/CacheRecordManager.mjs +18 -0
  43. package/dist/mjs/rendering/caching/RenderingCache.d.ts +1 -0
  44. package/dist/mjs/rendering/caching/RenderingCache.mjs +3 -0
  45. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +3 -2
  46. package/dist/mjs/tools/SelectionTool/Selection.d.ts +5 -4
  47. package/dist/mjs/tools/SelectionTool/Selection.mjs +75 -48
  48. package/dist/mjs/tools/SelectionTool/SelectionHandle.d.ts +2 -2
  49. package/dist/mjs/tools/SelectionTool/SelectionHandle.mjs +8 -3
  50. package/dist/mjs/tools/SelectionTool/SelectionTool.d.ts +3 -1
  51. package/dist/mjs/tools/SelectionTool/SelectionTool.mjs +36 -16
  52. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.d.ts +23 -0
  53. package/dist/mjs/tools/SelectionTool/ToPointerAutoscroller.mjs +77 -0
  54. package/dist/mjs/tools/SelectionTool/TransformMode.d.ts +10 -3
  55. package/dist/mjs/tools/SelectionTool/TransformMode.mjs +52 -9
  56. package/dist/mjs/util/listenForKeyboardEventsFrom.d.ts +16 -0
  57. package/dist/mjs/util/listenForKeyboardEventsFrom.mjs +140 -0
  58. package/dist/mjs/version.mjs +1 -1
  59. package/package.json +4 -4
  60. package/src/tools/SelectionTool/SelectionTool.scss +62 -9
@@ -21,7 +21,7 @@ class DragTransformer {
21
21
  this.selection.setTransform(math_1.Mat33.translation(delta));
22
22
  }
23
23
  onDragEnd() {
24
- this.selection.finalizeTransform();
24
+ return this.selection.finalizeTransform();
25
25
  }
26
26
  }
27
27
  exports.DragTransformer = DragTransformer;
@@ -35,6 +35,32 @@ class ResizeTransformer {
35
35
  this.selection.setTransform(math_1.Mat33.identity);
36
36
  this.mode = mode;
37
37
  this.dragStartPoint = startPoint;
38
+ this.computeOriginAndScaleRate();
39
+ }
40
+ computeOriginAndScaleRate() {
41
+ // Store the index of the furthest corner from startPoint. We'll use that
42
+ // to determine where the transform considers (0, 0) (where we scale from).
43
+ const selectionRect = this.selection.preTransformRegion;
44
+ const selectionBoxCorners = selectionRect.corners;
45
+ let largestDistSquared = 0;
46
+ for (let i = 0; i < selectionBoxCorners.length; i++) {
47
+ const currentCorner = selectionBoxCorners[i];
48
+ const distSquaredToCurrent = this.dragStartPoint.minus(currentCorner).magnitudeSquared();
49
+ if (distSquaredToCurrent > largestDistSquared) {
50
+ largestDistSquared = distSquaredToCurrent;
51
+ this.transformOrigin = currentCorner;
52
+ }
53
+ }
54
+ // Determine whether moving the mouse to the right increases or decreases the width.
55
+ let widthScaleRate = 1;
56
+ let heightScaleRate = 1;
57
+ if (this.transformOrigin.x > selectionRect.center.x) {
58
+ widthScaleRate = -1;
59
+ }
60
+ if (this.transformOrigin.y > selectionRect.center.y) {
61
+ heightScaleRate = -1;
62
+ }
63
+ this.scaleRate = math_1.Vec2.of(widthScaleRate, heightScaleRate);
38
64
  }
39
65
  onDragUpdate(canvasPos) {
40
66
  const canvasDelta = canvasPos.minus(this.dragStartPoint);
@@ -42,11 +68,11 @@ class ResizeTransformer {
42
68
  const origHeight = this.selection.preTransformRegion.height;
43
69
  let scale = math_1.Vec2.of(1, 1);
44
70
  if (this.mode === types_1.ResizeMode.HorizontalOnly) {
45
- const newWidth = origWidth + canvasDelta.x;
71
+ const newWidth = origWidth + canvasDelta.x * this.scaleRate.x;
46
72
  scale = math_1.Vec2.of(newWidth / origWidth, scale.y);
47
73
  }
48
74
  if (this.mode === types_1.ResizeMode.VerticalOnly) {
49
- const newHeight = origHeight + canvasDelta.y;
75
+ const newHeight = origHeight + canvasDelta.y * this.scaleRate.y;
50
76
  scale = math_1.Vec2.of(scale.x, newHeight / origHeight);
51
77
  }
52
78
  if (this.mode === types_1.ResizeMode.Both) {
@@ -58,12 +84,12 @@ class ResizeTransformer {
58
84
  // long decimal representations => large file sizes.
59
85
  scale = scale.map(component => Viewport_1.default.roundScaleRatio(component, 2));
60
86
  if (scale.x !== 0 && scale.y !== 0) {
61
- const origin = this.editor.viewport.roundPoint(this.selection.preTransformRegion.topLeft);
87
+ const origin = this.editor.viewport.roundPoint(this.transformOrigin);
62
88
  this.selection.setTransform(math_1.Mat33.scaling2D(scale, origin));
63
89
  }
64
90
  }
65
91
  onDragEnd() {
66
- this.selection.finalizeTransform();
92
+ return this.selection.finalizeTransform();
67
93
  }
68
94
  }
69
95
  exports.ResizeTransformer = ResizeTransformer;
@@ -72,6 +98,8 @@ class RotateTransformer {
72
98
  this.editor = editor;
73
99
  this.selection = selection;
74
100
  this.startAngle = 0;
101
+ this.targetRotation = 0;
102
+ this.maximumDistFromStart = 0;
75
103
  }
76
104
  getAngle(canvasPoint) {
77
105
  const selectionCenter = this.selection.preTransformRegion.center;
@@ -84,14 +112,16 @@ class RotateTransformer {
84
112
  return Math.round(angle * roundingFactor) / roundingFactor;
85
113
  }
86
114
  onDragStart(startPoint) {
115
+ this.startPoint = startPoint;
87
116
  this.selection.setTransform(math_1.Mat33.identity);
88
117
  this.startAngle = this.getAngle(startPoint);
118
+ this.maximumDistFromStart = 0;
119
+ this.targetRotation = 0;
89
120
  }
90
- onDragUpdate(canvasPos) {
91
- const targetRotation = this.roundAngle(this.getAngle(canvasPos) - this.startAngle);
121
+ setRotationTo(angle) {
92
122
  // Transform in canvas space
93
123
  const canvasSelCenter = this.editor.viewport.roundPoint(this.selection.preTransformRegion.center);
94
- const unrounded = math_1.Mat33.zRotation(targetRotation);
124
+ const unrounded = math_1.Mat33.zRotation(angle);
95
125
  const roundedRotationTransform = unrounded.mapEntries(entry => Viewport_1.default.roundScaleRatio(entry));
96
126
  const fullRoundedTransform = math_1.Mat33
97
127
  .translation(canvasSelCenter)
@@ -99,8 +129,21 @@ class RotateTransformer {
99
129
  .rightMul(math_1.Mat33.translation(canvasSelCenter.times(-1)));
100
130
  this.selection.setTransform(fullRoundedTransform);
101
131
  }
132
+ onDragUpdate(canvasPos) {
133
+ this.targetRotation = this.roundAngle(this.getAngle(canvasPos) - this.startAngle);
134
+ this.setRotationTo(this.targetRotation);
135
+ const distFromStart = canvasPos.minus(this.startPoint).magnitude();
136
+ if (distFromStart > this.maximumDistFromStart) {
137
+ this.maximumDistFromStart = distFromStart;
138
+ }
139
+ }
102
140
  onDragEnd() {
103
- this.selection.finalizeTransform();
141
+ // Anything less than this is considered a click
142
+ const clickThreshold = 15;
143
+ if (this.maximumDistFromStart < clickThreshold && this.targetRotation === 0) {
144
+ this.setRotationTo(-Math.PI / 2);
145
+ }
146
+ return this.selection.finalizeTransform();
104
147
  }
105
148
  }
106
149
  exports.RotateTransformer = RotateTransformer;
@@ -0,0 +1,16 @@
1
+ interface Callbacks {
2
+ filter(event: KeyboardEvent): boolean;
3
+ handleKeyDown(event: KeyboardEvent): void;
4
+ handleKeyUp(event: KeyboardEvent): void;
5
+ }
6
+ /**
7
+ * Calls `callbacks` when different keys are known to be pressed.
8
+ *
9
+ * `filter` can be used to ignore events.
10
+ *
11
+ * This includes keys that didn't trigger a keydown or keyup event, but did cause
12
+ * shiftKey/altKey/metaKey/etc. properties to change on other events (e.g. mousemove
13
+ * events). Artifical events are created for these changes and sent to `callbacks`.
14
+ */
15
+ declare const listenForKeyboardEventsFrom: (elem: HTMLElement, callbacks: Callbacks) => void;
16
+ export default listenForKeyboardEventsFrom;
@@ -0,0 +1,142 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ /**
4
+ * Calls `callbacks` when different keys are known to be pressed.
5
+ *
6
+ * `filter` can be used to ignore events.
7
+ *
8
+ * This includes keys that didn't trigger a keydown or keyup event, but did cause
9
+ * shiftKey/altKey/metaKey/etc. properties to change on other events (e.g. mousemove
10
+ * events). Artifical events are created for these changes and sent to `callbacks`.
11
+ */
12
+ const listenForKeyboardEventsFrom = (elem, callbacks) => {
13
+ // Track which keys are down so we can release them when the element
14
+ // loses focus. This is particularly important for keys like Control
15
+ // that can trigger shortcuts that cause the editor to lose focus before
16
+ // the keyup event is triggered.
17
+ let keysDown = [];
18
+ // Return whether two objects that are similar to keyboard events represent the
19
+ // same key.
20
+ const keyEventsMatch = (a, b) => {
21
+ return a.key === b.key && a.code === b.code;
22
+ };
23
+ const isKeyDown = (keyEvent) => {
24
+ return keysDown.some(other => keyEventsMatch(other, keyEvent));
25
+ };
26
+ const keyEventToRecord = (event) => {
27
+ return {
28
+ code: event.code,
29
+ key: event.key,
30
+ ctrlKey: event.ctrlKey,
31
+ altKey: event.altKey,
32
+ shiftKey: event.shiftKey,
33
+ metaKey: event.metaKey,
34
+ };
35
+ };
36
+ const handleKeyEvent = (htmlEvent) => {
37
+ if (htmlEvent.type === 'keydown') {
38
+ // Add event to the list of keys that are down (so long as it
39
+ // isn't a duplicate).
40
+ if (!isKeyDown(htmlEvent)) {
41
+ // Destructructring, then pushing seems to cause
42
+ // data loss. Copy properties individually:
43
+ keysDown.push(keyEventToRecord(htmlEvent));
44
+ }
45
+ if (!callbacks.filter(htmlEvent)) {
46
+ return;
47
+ }
48
+ callbacks.handleKeyDown(htmlEvent);
49
+ }
50
+ else { // keyup
51
+ console.assert(htmlEvent.type === 'keyup');
52
+ // Remove the key from keysDown -- it's no longer down.
53
+ keysDown = keysDown.filter(event => {
54
+ const matches = keyEventsMatch(event, htmlEvent);
55
+ return !matches;
56
+ });
57
+ if (!callbacks.filter(htmlEvent)) {
58
+ return;
59
+ }
60
+ callbacks.handleKeyUp(htmlEvent);
61
+ }
62
+ };
63
+ elem.addEventListener('keydown', htmlEvent => {
64
+ handleKeyEvent(htmlEvent);
65
+ });
66
+ elem.addEventListener('keyup', htmlEvent => {
67
+ handleKeyEvent(htmlEvent);
68
+ });
69
+ elem.addEventListener('focusout', (focusEvent) => {
70
+ const stillHasFocus = focusEvent.relatedTarget && elem.contains(focusEvent.relatedTarget);
71
+ if (!stillHasFocus) {
72
+ for (const event of keysDown) {
73
+ callbacks.handleKeyUp(new KeyboardEvent('keyup', {
74
+ ...event,
75
+ }));
76
+ }
77
+ keysDown = [];
78
+ }
79
+ });
80
+ const fireArtificalEventsBasedOn = (htmlEvent) => {
81
+ let wasShiftDown = false;
82
+ let wasCtrlDown = false;
83
+ let wasAltDown = false;
84
+ let wasMetaDown = false;
85
+ for (const otherEvent of keysDown) {
86
+ const code = otherEvent.code;
87
+ wasShiftDown ||= !!code.match(/^Shift(Left|Right)$/);
88
+ wasCtrlDown ||= !!code.match(/^Control(Left|Right)$/);
89
+ wasAltDown ||= !!code.match(/^Alt(Left|Right)$/);
90
+ wasMetaDown ||= !!code.match(/^Meta(Left|Right)$/);
91
+ }
92
+ const eventName = (isDown) => {
93
+ if (isDown) {
94
+ return 'keydown';
95
+ }
96
+ else {
97
+ return 'keyup';
98
+ }
99
+ };
100
+ const eventInitDefaults = {
101
+ shiftKey: htmlEvent.shiftKey,
102
+ altKey: htmlEvent.altKey,
103
+ metaKey: htmlEvent.metaKey,
104
+ ctrlKey: htmlEvent.ctrlKey,
105
+ };
106
+ if (htmlEvent.shiftKey !== wasShiftDown) {
107
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.shiftKey), {
108
+ ...eventInitDefaults,
109
+ key: 'Shift',
110
+ code: 'ShiftLeft',
111
+ }));
112
+ }
113
+ if (htmlEvent.altKey !== wasAltDown) {
114
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.altKey), {
115
+ ...eventInitDefaults,
116
+ key: 'Alt',
117
+ code: 'AltLeft',
118
+ }));
119
+ }
120
+ if (htmlEvent.ctrlKey !== wasCtrlDown) {
121
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.ctrlKey), {
122
+ ...eventInitDefaults,
123
+ key: 'Control',
124
+ code: 'ControlLeft',
125
+ }));
126
+ }
127
+ if (htmlEvent.metaKey !== wasMetaDown) {
128
+ handleKeyEvent(new KeyboardEvent(eventName(htmlEvent.metaKey), {
129
+ ...eventInitDefaults,
130
+ key: 'Meta',
131
+ code: 'MetaLeft',
132
+ }));
133
+ }
134
+ };
135
+ elem.addEventListener('mousedown', (htmlEvent) => {
136
+ fireArtificalEventsBasedOn(htmlEvent);
137
+ });
138
+ elem.addEventListener('mousemove', (htmlEvent) => {
139
+ fireArtificalEventsBasedOn(htmlEvent);
140
+ });
141
+ };
142
+ exports.default = listenForKeyboardEventsFrom;
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = {
4
- number: '1.6.1',
4
+ number: '1.7.0',
5
5
  };
@@ -70,6 +70,7 @@ export interface EditorSettings {
70
70
  */
71
71
  appInfo: {
72
72
  name: string;
73
+ description?: string;
73
74
  version?: string;
74
75
  } | null;
75
76
  }
@@ -271,6 +272,10 @@ export declare class Editor {
271
272
  /** Remove all event listeners registered by this function. */
272
273
  remove: () => void;
273
274
  };
275
+ /** @internal */
276
+ protected handleHTMLKeyDownEvent(htmlEvent: KeyboardEvent): void;
277
+ /** @internal */
278
+ protected handleHTMLKeyUpEvent(htmlEvent: KeyboardEvent): void;
274
279
  /**
275
280
  * Adds event listners for keypresses (and drop events) on `elem` and forwards those
276
281
  * events to the editor.
@@ -27,6 +27,7 @@ import makeAboutDialog from './dialogs/makeAboutDialog.mjs';
27
27
  import version from './version.mjs';
28
28
  import { editorImageToSVGSync, editorImageToSVGAsync } from './image/export/editorImageToSVG.mjs';
29
29
  import { MutableReactiveValue } from './util/ReactiveValue.mjs';
30
+ import listenForKeyboardEventsFrom from './util/listenForKeyboardEventsFrom.mjs';
30
31
  /**
31
32
  * The main entrypoint for the full editor.
32
33
  *
@@ -592,6 +593,29 @@ export class Editor {
592
593
  return sendToEditor;
593
594
  }, otherEventsFilter);
594
595
  }
596
+ /** @internal */
597
+ handleHTMLKeyDownEvent(htmlEvent) {
598
+ console.assert(htmlEvent.type === 'keydown', `handling a keydown event with type ${htmlEvent.type}`);
599
+ const event = keyPressEventFromHTMLEvent(htmlEvent);
600
+ if (this.toolController.dispatchInputEvent(event)) {
601
+ htmlEvent.preventDefault();
602
+ }
603
+ else if (event.key === 't' || event.key === 'T') {
604
+ htmlEvent.preventDefault();
605
+ this.display.rerenderAsText();
606
+ }
607
+ else if (event.key === 'Escape') {
608
+ this.renderingRegion.blur();
609
+ }
610
+ }
611
+ /** @internal */
612
+ handleHTMLKeyUpEvent(htmlEvent) {
613
+ console.assert(htmlEvent.type === 'keyup', `Handling a keyup event with type ${htmlEvent.type}`);
614
+ const event = keyUpEventFromHTMLEvent(htmlEvent);
615
+ if (this.toolController.dispatchInputEvent(event)) {
616
+ htmlEvent.preventDefault();
617
+ }
618
+ }
595
619
  /**
596
620
  * Adds event listners for keypresses (and drop events) on `elem` and forwards those
597
621
  * events to the editor.
@@ -600,62 +624,14 @@ export class Editor {
600
624
  * passed to the editor.
601
625
  */
602
626
  handleKeyEventsFrom(elem, filter = () => true) {
603
- // Track which keys are down so we can release them when the element
604
- // loses focus. This is particularly important for keys like Control
605
- // that can trigger shortcuts that cause the editor to lose focus before
606
- // the keyup event is triggered.
607
- let keysDown = [];
608
- // Return whether two objects that are similar to keyboard events represent the
609
- // same key.
610
- const keyEventsMatch = (a, b) => {
611
- return a.key === b.key && a.code === b.code;
612
- };
613
- elem.addEventListener('keydown', htmlEvent => {
614
- if (!filter(htmlEvent)) {
615
- return;
616
- }
617
- const event = keyPressEventFromHTMLEvent(htmlEvent);
618
- // Add event to the list of keys that are down (so long as it
619
- // isn't a duplicate).
620
- if (!keysDown.some(other => keyEventsMatch(other, event))) {
621
- keysDown.push(event);
622
- }
623
- if (event.key === 't' || event.key === 'T') {
624
- htmlEvent.preventDefault();
625
- this.display.rerenderAsText();
626
- }
627
- else if (this.toolController.dispatchInputEvent(event)) {
628
- htmlEvent.preventDefault();
629
- }
630
- else if (event.key === 'Escape') {
631
- this.renderingRegion.blur();
632
- }
633
- });
634
- elem.addEventListener('keyup', htmlEvent => {
635
- // Remove the key from keysDown -- it's no longer down.
636
- keysDown = keysDown.filter(event => {
637
- const matches = keyEventsMatch(event, htmlEvent);
638
- return !matches;
639
- });
640
- if (!filter(htmlEvent)) {
641
- return;
642
- }
643
- const event = keyUpEventFromHTMLEvent(htmlEvent);
644
- if (this.toolController.dispatchInputEvent(event)) {
645
- htmlEvent.preventDefault();
646
- }
647
- });
648
- elem.addEventListener('focusout', (event) => {
649
- const stillHasFocus = event.relatedTarget && elem.contains(event.relatedTarget);
650
- if (!stillHasFocus) {
651
- for (const event of keysDown) {
652
- this.toolController.dispatchInputEvent({
653
- ...event,
654
- kind: InputEvtType.KeyUpEvent,
655
- });
656
- }
657
- keysDown = [];
658
- }
627
+ listenForKeyboardEventsFrom(elem, {
628
+ filter,
629
+ handleKeyDown: (htmlEvent) => {
630
+ this.handleHTMLKeyDownEvent(htmlEvent);
631
+ },
632
+ handleKeyUp: (htmlEvent) => {
633
+ this.handleHTMLKeyUpEvent(htmlEvent);
634
+ },
659
635
  });
660
636
  // Allow drop.
661
637
  elem.ondragover = evt => {
@@ -1012,7 +988,6 @@ export class Editor {
1012
988
  this.display.setDraftMode(true);
1013
989
  const originalBackgrounds = this.image.getBackgroundComponents();
1014
990
  const eraseBackgroundCommand = new Erase(originalBackgrounds);
1015
- let autoresizeEnabled = false;
1016
991
  await loader.start(async (component) => {
1017
992
  await this.dispatchNoAnnounce(EditorImage.addElement(component));
1018
993
  }, (countProcessed, totalToProcess) => {
@@ -1026,13 +1001,9 @@ export class Editor {
1026
1001
  this.dispatchNoAnnounce(this.setImportExportRect(importExportRect), false);
1027
1002
  this.dispatchNoAnnounce(this.viewport.zoomTo(importExportRect), false);
1028
1003
  if (options) {
1029
- autoresizeEnabled = options.autoresize;
1004
+ this.dispatchNoAnnounce(this.image.setAutoresizeEnabled(options.autoresize), false);
1030
1005
  }
1031
1006
  });
1032
- // TODO: Move this call into the callback above. Currently, this would cause
1033
- // decrease in performance as the main background would be repeatedly added
1034
- // and removed from the editor every time another component is added.
1035
- this.dispatchNoAnnounce(this.image.setAutoresizeEnabled(autoresizeEnabled), false);
1036
1007
  // Ensure that we don't have multiple overlapping BackgroundComponents. Remove
1037
1008
  // old BackgroundComponents.
1038
1009
  // Overlapping BackgroundComponents may cause changing the background color to
@@ -1122,15 +1093,18 @@ export class Editor {
1122
1093
  const iconLicenseText = this.icons.licenseInfo();
1123
1094
  const notices = [];
1124
1095
  if (this.settings.appInfo) {
1125
- const versionLines = [];
1096
+ const descriptionLines = [];
1126
1097
  if (this.settings.appInfo.version) {
1127
- versionLines.push(`v${this.settings.appInfo.version}`, '');
1098
+ descriptionLines.push(`v${this.settings.appInfo.version}`, '');
1099
+ }
1100
+ if (this.settings.appInfo.description) {
1101
+ descriptionLines.push(this.settings.appInfo.description + '\n');
1128
1102
  }
1129
1103
  notices.push({
1130
1104
  heading: `${this.settings.appInfo.name}`,
1131
1105
  text: [
1132
- ...versionLines,
1133
- `Image editor library: js-draw v${version.number}.`,
1106
+ ...descriptionLines,
1107
+ `(js-draw v${version.number})`,
1134
1108
  ].join('\n'),
1135
1109
  });
1136
1110
  }
@@ -1140,19 +1114,28 @@ export class Editor {
1140
1114
  text: `v${version.number}`,
1141
1115
  });
1142
1116
  }
1117
+ const screenSize = this.viewport.getScreenRectSize();
1143
1118
  notices.push({
1144
- heading: 'Developer information',
1119
+ heading: this.localization.developerInformation,
1145
1120
  text: [
1146
1121
  'Image debug information (from when this dialog was opened):',
1147
- ` ${this.viewport.getScaleFactor()}x zoom, ${180 / Math.PI * this.viewport.getRotationAngle()} rotation`,
1122
+ ` ${this.viewport.getScaleFactor()}x zoom, ${180 / Math.PI * this.viewport.getRotationAngle()}° rotation`,
1148
1123
  ` ${this.image.estimateNumElements()} components`,
1149
- ` ${this.getImportExportRect().w}x${this.getImportExportRect().h} size`,
1124
+ ` auto-resize: ${this.image.getAutoresizeEnabled() ? 'enabled' : 'disabled'}`,
1125
+ ` ${this.getImportExportRect().w}x${this.getImportExportRect().h} image size`,
1126
+ ` ${screenSize.x}x${screenSize.y} screen size`,
1127
+ ' cache:',
1128
+ ` ${this.display.getCache().getDebugInfo()
1129
+ // Indent
1130
+ .replace(/([\n])/g, '\n ')}`,
1150
1131
  ].join('\n'),
1151
1132
  minimized: true,
1152
1133
  });
1153
1134
  notices.push({
1154
- heading: 'Libraries',
1135
+ heading: this.localization.softwareLibraries,
1155
1136
  text: [
1137
+ `This image editor is powered by js-draw v${version.number}.`,
1138
+ '',
1156
1139
  'js-draw uses several libraries at runtime. Particularly noteworthy are:',
1157
1140
  ' - The Coloris color picker: https://github.com/mdbassit/Coloris',
1158
1141
  ' - The bezier.js Bézier curve library: https://github.com/Pomax/bezierjs'
@@ -122,7 +122,12 @@ export default class BackgroundComponent extends AbstractComponent {
122
122
  let needsRerender = false;
123
123
  if (!this.contentBBox.eq(importExportRect)) {
124
124
  this.contentBBox = importExportRect;
125
- needsRerender = true;
125
+ // If the box already fills the screen, rerendering it will have
126
+ // no visual effect.
127
+ //
128
+ // TODO: This decision should be made by queueRerenderOf and not here.
129
+ //
130
+ needsRerender ||= !this.fillsScreen;
126
131
  }
127
132
  const imageAutoresizes = image.getAutoresizeEnabled();
128
133
  if (imageAutoresizes !== this.fillsScreen) {
@@ -42,7 +42,7 @@ export default class TextComponent extends AbstractComponent implements Restylea
42
42
  private computeUntransformedBBoxOfPart;
43
43
  private recomputeBBox;
44
44
  private renderInternal;
45
- render(canvas: AbstractRenderer, _visibleRect?: Rect2): void;
45
+ render(canvas: AbstractRenderer, visibleRect?: Rect2): void;
46
46
  getProportionalRenderingTime(): number;
47
47
  intersects(lineSegment: LineSegment2): boolean;
48
48
  getStyle(): ComponentStyle;
@@ -99,30 +99,33 @@ class TextComponent extends AbstractComponent {
99
99
  let bbox = null;
100
100
  const cursor = new TextComponent.TextCursor(this.transform, this.style);
101
101
  for (const textObject of this.textObjects) {
102
- const transform = cursor.update(textObject);
102
+ const transform = cursor.update(textObject).transform;
103
103
  const currentBBox = this.computeUntransformedBBoxOfPart(textObject).transformedBoundingBox(transform);
104
104
  bbox ??= currentBBox;
105
105
  bbox = bbox.union(currentBBox);
106
106
  }
107
107
  this.contentBBox = bbox ?? Rect2.empty;
108
108
  }
109
- renderInternal(canvas) {
109
+ renderInternal(canvas, visibleRect) {
110
110
  const cursor = new TextComponent.TextCursor(this.transform, this.style);
111
111
  for (const textObject of this.textObjects) {
112
- const transform = cursor.update(textObject);
112
+ const { transform, bbox } = cursor.update(textObject);
113
+ if (visibleRect && !visibleRect.intersects(bbox)) {
114
+ continue;
115
+ }
113
116
  if (typeof textObject === 'string') {
114
117
  canvas.drawText(textObject, transform, this.style);
115
118
  }
116
119
  else {
117
120
  canvas.pushTransform(transform);
118
- textObject.renderInternal(canvas);
121
+ textObject.renderInternal(canvas, visibleRect?.transformedBoundingBox(transform.inverse()));
119
122
  canvas.popTransform();
120
123
  }
121
124
  }
122
125
  }
123
- render(canvas, _visibleRect) {
126
+ render(canvas, visibleRect) {
124
127
  canvas.startObject(this.contentBBox);
125
- this.renderInternal(canvas);
128
+ this.renderInternal(canvas, visibleRect);
126
129
  canvas.endObject(this.getLoadSaveData());
127
130
  }
128
131
  getProportionalRenderingTime() {
@@ -132,7 +135,7 @@ class TextComponent extends AbstractComponent {
132
135
  const cursor = new TextComponent.TextCursor(this.transform, this.style);
133
136
  for (const subObject of this.textObjects) {
134
137
  // Convert canvas space to internal space relative to the current object.
135
- const invTransform = cursor.update(subObject).inverse();
138
+ const invTransform = cursor.update(subObject).transform.inverse();
136
139
  const transformedLine = lineSegment.transformedBy(invTransform);
137
140
  if (typeof subObject === 'string') {
138
141
  const textBBox = TextComponent.getTextDimens(subObject, this.style);
@@ -310,11 +313,11 @@ TextComponent.TextCursor = class {
310
313
  this.transform = Mat33.identity;
311
314
  }
312
315
  /**
313
- * Based on previous calls to `update`, returns the transformation of
314
- * the given `element` (including the parentTransform given to this cursor's
315
- * constructor).
316
+ * Based on previous calls to `update`, returns the transformation and bounding box (relative
317
+ * to the parent element, or if none, the canvas) of the given `element`. Note that
318
+ * this is computed in part using the `parentTransform` provivded to this cursor's constructor.
316
319
  *
317
- * The result does not take into account
320
+ * Warning: There may be edge cases here that are not taken into account.
318
321
  */
319
322
  update(elem) {
320
323
  let elementTransform = Mat33.identity;
@@ -353,7 +356,11 @@ TextComponent.TextCursor = class {
353
356
  // Update this.transform so that future calls to update return correct values.
354
357
  const endShiftTransform = Mat33.translation(Vec2.of(textSize.width, 0));
355
358
  this.transform = elementTransform.rightMul(elemInternalTransform).rightMul(endShiftTransform);
356
- return this.parentTransform.rightMul(elementTransform);
359
+ const transform = this.parentTransform.rightMul(elementTransform);
360
+ return {
361
+ transform,
362
+ bbox: textSize.transformedBoundingBox(transform),
363
+ };
357
364
  }
358
365
  };
359
366
  export default TextComponent;
@@ -10,5 +10,7 @@ export interface EditorLocalization extends ToolbarLocalization, ToolLocalizatio
10
10
  doneLoading: string;
11
11
  loading: (percentage: number) => string;
12
12
  imageEditor: string;
13
+ softwareLibraries: string;
14
+ developerInformation: string;
13
15
  }
14
16
  export declare const defaultEditorLocalization: EditorLocalization;
@@ -19,4 +19,6 @@ export const defaultEditorLocalization = {
19
19
  doneLoading: 'Done loading',
20
20
  undoAnnouncement: (commandDescription) => `Undid ${commandDescription}`,
21
21
  redoAnnouncement: (commandDescription) => `Redid ${commandDescription}`,
22
+ softwareLibraries: 'Libraries',
23
+ developerInformation: 'Developer information',
22
24
  };
@@ -2,6 +2,7 @@
2
2
  * Comments to help translators create translations.
3
3
  */
4
4
  const comments = {
5
+ pen: 'Likely unused',
5
6
  dragAndDropHereOrBrowse: 'Uses {{curly braces}} to denote bold text',
6
7
  closeSidebar: 'Currently used as an accessibilty label',
7
8
  };
@@ -21,11 +21,26 @@ export const visualEquivalent = (renderablePath, visibleRect) => {
21
21
  const path = pathFromRenderable(renderablePath);
22
22
  const strokeWidth = renderablePath.style.stroke?.width ?? 0;
23
23
  const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
24
+ const styledPathBBox = path.bbox.grownBy(strokeWidth);
25
+ // Are we close enough to the path that it fills the entire screen?
26
+ if (onlyStroked
27
+ && renderablePath.style.stroke
28
+ && strokeWidth > visibleRect.maxDimension
29
+ && styledPathBBox.containsRect(visibleRect)) {
30
+ const strokeRadius = strokeWidth / 2;
31
+ // Do a fast, but with many false negatives, check.
32
+ for (const point of path.startEndPoints()) {
33
+ // If within the strokeRadius of any point
34
+ if (visibleRect.isWithinRadiusOf(strokeRadius, point)) {
35
+ return pathToRenderable(Path.fromRect(visibleRect), { fill: renderablePath.style.stroke.color });
36
+ }
37
+ }
38
+ }
24
39
  // Scale the expanded rect --- the visual equivalent is only close for huge strokes.
25
40
  const expandedRect = visibleRect.grownBy(strokeWidth)
26
41
  .transformedBoundingBox(Mat33.scaling2D(4, visibleRect.center));
27
42
  // TODO: Handle simplifying very small paths.
28
- if (expandedRect.containsRect(path.bbox.grownBy(strokeWidth))) {
43
+ if (expandedRect.containsRect(styledPathBBox)) {
29
44
  return renderablePath;
30
45
  }
31
46
  const parts = [];
@@ -9,4 +9,5 @@ export declare class CacheRecordManager {
9
9
  setSharedState(state: CacheState): void;
10
10
  allocCanvas(drawTo: Rect2, onDealloc: BeforeDeallocCallback): CacheRecord;
11
11
  private getLeastRecentlyUsedRecord;
12
+ getDebugInfo(): string;
12
13
  }