js-draw 0.1.11 → 0.2.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 (220) hide show
  1. package/.eslintrc.js +1 -0
  2. package/.firebaserc +5 -0
  3. package/.github/workflows/firebase-hosting-merge.yml +25 -0
  4. package/.github/workflows/firebase-hosting-pull-request.yml +22 -0
  5. package/.github/workflows/github-pages.yml +52 -0
  6. package/CHANGELOG.md +13 -0
  7. package/README.md +11 -6
  8. package/dist/bundle.js +1 -1
  9. package/dist/src/Color4.d.ts +19 -0
  10. package/dist/src/Color4.js +24 -3
  11. package/dist/src/Editor.d.ts +133 -4
  12. package/dist/src/Editor.js +124 -27
  13. package/dist/src/EditorImage.d.ts +8 -3
  14. package/dist/src/EditorImage.js +42 -26
  15. package/dist/src/EventDispatcher.d.ts +18 -0
  16. package/dist/src/EventDispatcher.js +19 -4
  17. package/dist/src/Pointer.d.ts +1 -1
  18. package/dist/src/Pointer.js +4 -3
  19. package/dist/src/SVGLoader.d.ts +1 -1
  20. package/dist/src/SVGLoader.js +14 -6
  21. package/dist/src/UndoRedoHistory.js +15 -2
  22. package/dist/src/Viewport.d.ts +8 -25
  23. package/dist/src/Viewport.js +18 -10
  24. package/dist/src/bundle/bundled.d.ts +1 -2
  25. package/dist/src/bundle/bundled.js +1 -2
  26. package/dist/src/commands/Command.d.ts +2 -2
  27. package/dist/src/commands/Command.js +4 -4
  28. package/dist/src/commands/Duplicate.d.ts +2 -2
  29. package/dist/src/commands/Duplicate.js +4 -5
  30. package/dist/src/commands/Erase.d.ts +2 -2
  31. package/dist/src/commands/Erase.js +7 -6
  32. package/dist/src/commands/SerializableCommand.d.ts +4 -5
  33. package/dist/src/commands/SerializableCommand.js +12 -4
  34. package/dist/src/commands/invertCommand.d.ts +4 -0
  35. package/dist/src/commands/invertCommand.js +44 -0
  36. package/dist/src/commands/lib.d.ts +6 -0
  37. package/dist/src/commands/lib.js +6 -0
  38. package/dist/src/commands/localization.d.ts +2 -1
  39. package/dist/src/commands/localization.js +1 -0
  40. package/dist/src/components/AbstractComponent.d.ts +16 -11
  41. package/dist/src/components/AbstractComponent.js +28 -17
  42. package/dist/src/components/SVGGlobalAttributesObject.d.ts +4 -4
  43. package/dist/src/components/SVGGlobalAttributesObject.js +8 -2
  44. package/dist/src/components/Stroke.d.ts +16 -6
  45. package/dist/src/components/Stroke.js +12 -9
  46. package/dist/src/components/Text.d.ts +5 -5
  47. package/dist/src/components/Text.js +9 -9
  48. package/dist/src/components/UnknownSVGObject.d.ts +4 -4
  49. package/dist/src/components/UnknownSVGObject.js +7 -2
  50. package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
  51. package/dist/src/components/builders/ArrowBuilder.js +1 -1
  52. package/dist/src/components/builders/FreehandLineBuilder.d.ts +8 -3
  53. package/dist/src/components/builders/FreehandLineBuilder.js +142 -71
  54. package/dist/src/components/builders/LineBuilder.d.ts +1 -1
  55. package/dist/src/components/builders/LineBuilder.js +1 -1
  56. package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
  57. package/dist/src/components/builders/RectangleBuilder.js +3 -3
  58. package/dist/src/components/builders/types.d.ts +1 -1
  59. package/dist/src/components/lib.d.ts +4 -0
  60. package/dist/src/components/lib.js +4 -0
  61. package/dist/src/lib.d.ts +25 -0
  62. package/dist/src/lib.js +25 -0
  63. package/dist/src/localization.d.ts +1 -0
  64. package/dist/src/localization.js +5 -1
  65. package/dist/src/localizations/es.js +1 -1
  66. package/dist/src/{geometry → math}/LineSegment2.d.ts +0 -0
  67. package/dist/src/{geometry → math}/LineSegment2.js +0 -0
  68. package/dist/src/math/Mat33.d.ts +78 -0
  69. package/dist/src/{geometry → math}/Mat33.js +48 -20
  70. package/dist/src/{geometry → math}/Path.d.ts +2 -1
  71. package/dist/src/{geometry → math}/Path.js +59 -52
  72. package/dist/src/{geometry → math}/Rect2.d.ts +2 -2
  73. package/dist/src/{geometry → math}/Rect2.js +0 -0
  74. package/dist/src/{geometry → math}/Vec2.d.ts +0 -0
  75. package/dist/src/{geometry → math}/Vec2.js +0 -0
  76. package/dist/src/math/Vec3.d.ts +96 -0
  77. package/dist/src/{geometry → math}/Vec3.js +63 -15
  78. package/dist/src/math/lib.d.ts +7 -0
  79. package/dist/src/math/lib.js +7 -0
  80. package/dist/src/math/rounding.d.ts +3 -0
  81. package/dist/src/math/rounding.js +121 -0
  82. package/dist/src/rendering/Display.d.ts +47 -1
  83. package/dist/src/rendering/Display.js +60 -15
  84. package/dist/src/rendering/caching/CacheRecord.d.ts +3 -2
  85. package/dist/src/rendering/caching/CacheRecord.js +4 -1
  86. package/dist/src/rendering/caching/CacheRecordManager.d.ts +5 -4
  87. package/dist/src/rendering/caching/CacheRecordManager.js +16 -4
  88. package/dist/src/rendering/caching/RenderingCache.d.ts +2 -3
  89. package/dist/src/rendering/caching/RenderingCache.js +10 -11
  90. package/dist/src/rendering/caching/RenderingCacheNode.d.ts +2 -1
  91. package/dist/src/rendering/caching/RenderingCacheNode.js +18 -7
  92. package/dist/src/rendering/caching/testUtils.js +1 -1
  93. package/dist/src/rendering/caching/types.d.ts +2 -4
  94. package/dist/src/rendering/localization.d.ts +2 -0
  95. package/dist/src/rendering/localization.js +2 -0
  96. package/dist/src/rendering/renderers/AbstractRenderer.d.ts +4 -4
  97. package/dist/src/rendering/renderers/AbstractRenderer.js +2 -2
  98. package/dist/src/rendering/renderers/CanvasRenderer.d.ts +4 -4
  99. package/dist/src/rendering/renderers/CanvasRenderer.js +2 -2
  100. package/dist/src/rendering/renderers/DummyRenderer.d.ts +4 -4
  101. package/dist/src/rendering/renderers/DummyRenderer.js +1 -1
  102. package/dist/src/rendering/renderers/SVGRenderer.d.ts +3 -3
  103. package/dist/src/rendering/renderers/SVGRenderer.js +8 -2
  104. package/dist/src/rendering/renderers/TextOnlyRenderer.d.ts +5 -3
  105. package/dist/src/rendering/renderers/TextOnlyRenderer.js +13 -3
  106. package/dist/src/toolbar/HTMLToolbar.js +1 -0
  107. package/dist/src/toolbar/icons.d.ts +3 -0
  108. package/dist/src/toolbar/icons.js +142 -132
  109. package/dist/src/toolbar/localization.d.ts +2 -1
  110. package/dist/src/toolbar/localization.js +2 -1
  111. package/dist/src/toolbar/makeColorInput.js +3 -2
  112. package/dist/src/toolbar/widgets/ActionButtonWidget.d.ts +13 -0
  113. package/dist/src/toolbar/widgets/ActionButtonWidget.js +21 -0
  114. package/dist/src/toolbar/widgets/BaseWidget.js +2 -0
  115. package/dist/src/toolbar/widgets/HandToolWidget.js +3 -3
  116. package/dist/src/toolbar/widgets/PenWidget.js +1 -0
  117. package/dist/src/toolbar/widgets/SelectionWidget.d.ts +0 -1
  118. package/dist/src/toolbar/widgets/SelectionWidget.js +23 -30
  119. package/dist/src/tools/Eraser.js +1 -1
  120. package/dist/src/tools/PanZoom.d.ts +1 -1
  121. package/dist/src/tools/PanZoom.js +24 -14
  122. package/dist/src/tools/Pen.d.ts +1 -2
  123. package/dist/src/tools/Pen.js +8 -1
  124. package/dist/src/tools/PipetteTool.js +1 -0
  125. package/dist/src/tools/SelectionTool.d.ts +3 -3
  126. package/dist/src/tools/SelectionTool.js +51 -28
  127. package/dist/src/tools/TextTool.js +1 -1
  128. package/dist/src/types.d.ts +21 -10
  129. package/dist/src/types.js +7 -5
  130. package/firebase.json +16 -0
  131. package/package.json +118 -101
  132. package/src/Color4.ts +23 -2
  133. package/src/Editor.ts +181 -37
  134. package/src/EditorImage.test.ts +2 -4
  135. package/src/EditorImage.ts +46 -28
  136. package/src/EventDispatcher.ts +21 -6
  137. package/src/Pointer.ts +4 -3
  138. package/src/SVGLoader.ts +14 -6
  139. package/src/UndoRedoHistory.ts +18 -2
  140. package/src/Viewport.ts +23 -18
  141. package/src/bundle/bundled.ts +1 -2
  142. package/src/commands/Command.ts +5 -5
  143. package/src/commands/Duplicate.ts +4 -5
  144. package/src/commands/Erase.ts +7 -6
  145. package/src/commands/SerializableCommand.ts +17 -9
  146. package/src/commands/invertCommand.ts +51 -0
  147. package/src/commands/lib.ts +14 -0
  148. package/src/commands/localization.ts +3 -1
  149. package/src/components/AbstractComponent.ts +35 -24
  150. package/src/components/SVGGlobalAttributesObject.ts +11 -4
  151. package/src/components/Stroke.test.ts +4 -6
  152. package/src/components/Stroke.ts +15 -11
  153. package/src/components/Text.test.ts +2 -2
  154. package/src/components/Text.ts +9 -10
  155. package/src/components/UnknownSVGObject.ts +10 -4
  156. package/src/components/builders/ArrowBuilder.ts +2 -2
  157. package/src/components/builders/FreehandLineBuilder.ts +190 -80
  158. package/src/components/builders/LineBuilder.ts +2 -2
  159. package/src/components/builders/RectangleBuilder.ts +3 -3
  160. package/src/components/builders/types.ts +1 -1
  161. package/src/components/lib.ts +9 -0
  162. package/src/lib.ts +28 -0
  163. package/src/localization.ts +6 -0
  164. package/src/localizations/es.ts +2 -1
  165. package/src/{geometry → math}/LineSegment2.test.ts +0 -0
  166. package/src/{geometry → math}/LineSegment2.ts +0 -0
  167. package/src/{geometry → math}/Mat33.test.ts +0 -0
  168. package/src/{geometry → math}/Mat33.ts +48 -20
  169. package/src/{geometry → math}/Path.fromString.test.ts +0 -0
  170. package/src/{geometry → math}/Path.test.ts +0 -0
  171. package/src/{geometry → math}/Path.toString.test.ts +11 -2
  172. package/src/{geometry → math}/Path.ts +61 -58
  173. package/src/{geometry → math}/Rect2.test.ts +0 -0
  174. package/src/{geometry → math}/Rect2.ts +2 -2
  175. package/src/{geometry → math}/Vec2.test.ts +0 -0
  176. package/src/{geometry → math}/Vec2.ts +0 -0
  177. package/src/{geometry → math}/Vec3.test.ts +0 -0
  178. package/src/{geometry → math}/Vec3.ts +64 -16
  179. package/src/math/lib.ts +15 -0
  180. package/src/math/rounding.test.ts +40 -0
  181. package/src/math/rounding.ts +147 -0
  182. package/src/rendering/Display.ts +63 -15
  183. package/src/rendering/caching/CacheRecord.test.ts +3 -3
  184. package/src/rendering/caching/CacheRecord.ts +6 -2
  185. package/src/rendering/caching/CacheRecordManager.ts +34 -8
  186. package/src/rendering/caching/RenderingCache.test.ts +3 -3
  187. package/src/rendering/caching/RenderingCache.ts +11 -16
  188. package/src/rendering/caching/RenderingCacheNode.ts +23 -7
  189. package/src/rendering/caching/testUtils.ts +1 -1
  190. package/src/rendering/caching/types.ts +2 -7
  191. package/src/rendering/localization.ts +4 -0
  192. package/src/rendering/renderers/AbstractRenderer.ts +4 -4
  193. package/src/rendering/renderers/CanvasRenderer.ts +5 -5
  194. package/src/rendering/renderers/DummyRenderer.test.ts +2 -2
  195. package/src/rendering/renderers/DummyRenderer.ts +4 -4
  196. package/src/rendering/renderers/SVGRenderer.ts +10 -4
  197. package/src/rendering/renderers/TextOnlyRenderer.ts +17 -6
  198. package/src/toolbar/HTMLToolbar.ts +1 -0
  199. package/src/toolbar/icons.ts +157 -137
  200. package/src/toolbar/localization.ts +4 -2
  201. package/src/toolbar/makeColorInput.ts +3 -2
  202. package/src/toolbar/toolbar.css +1 -1
  203. package/src/toolbar/widgets/ActionButtonWidget.ts +31 -0
  204. package/src/toolbar/widgets/BaseWidget.ts +2 -0
  205. package/src/toolbar/widgets/HandToolWidget.ts +3 -3
  206. package/src/toolbar/widgets/PenWidget.ts +2 -0
  207. package/src/toolbar/widgets/SelectionWidget.ts +46 -41
  208. package/src/tools/Eraser.ts +2 -2
  209. package/src/tools/PanZoom.ts +28 -17
  210. package/src/tools/Pen.ts +11 -2
  211. package/src/tools/PipetteTool.ts +2 -0
  212. package/src/tools/SelectionTool.test.ts +2 -4
  213. package/src/tools/SelectionTool.ts +52 -24
  214. package/src/tools/TextTool.ts +2 -2
  215. package/src/tools/UndoRedoShortcut.test.ts +1 -1
  216. package/src/types.ts +23 -7
  217. package/tsconfig.json +4 -1
  218. package/typedoc.json +20 -0
  219. package/dist/src/geometry/Mat33.d.ts +0 -32
  220. package/dist/src/geometry/Vec3.d.ts +0 -34
@@ -0,0 +1,147 @@
1
+ // @packageDocumentation @internal
2
+
3
+ // Clean up stringified numbers
4
+ const cleanUpNumber = (text: string) => {
5
+ // Regular expression substitions can be somewhat expensive. Only do them
6
+ // if necessary.
7
+ const lastChar = text.charAt(text.length - 1);
8
+ if (lastChar === '0' || lastChar === '.') {
9
+ // Remove trailing zeroes
10
+ text = text.replace(/([.]\d*[^0]+)0+$/, '$1');
11
+ text = text.replace(/[.]0+$/, '.');
12
+
13
+ // Remove trailing period
14
+ text = text.replace(/[.]$/, '');
15
+
16
+ if (text === '-0') {
17
+ return '0';
18
+ }
19
+ }
20
+
21
+ const firstChar = text.charAt(0);
22
+ if (firstChar === '0' || firstChar === '-') {
23
+ // Remove unnecessary leading zeroes.
24
+ text = text.replace(/^(0+)[.]/, '.');
25
+ text = text.replace(/^-(0+)[.]/, '-.');
26
+ }
27
+
28
+ return text;
29
+ };
30
+
31
+ export const toRoundedString = (num: number): string => {
32
+ // Try to remove rounding errors. If the number ends in at least three/four zeroes
33
+ // (or nines) just one or two digits, it's probably a rounding error.
34
+ const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d{1,2}$/;
35
+ const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,})\d{1,2}$/;
36
+
37
+ let text = num.toString(10);
38
+ if (text.indexOf('.') === -1) {
39
+ return text;
40
+ }
41
+
42
+ const roundingDownMatch = hasRoundingDownExp.exec(text);
43
+ if (roundingDownMatch) {
44
+ const negativeSign = roundingDownMatch[1];
45
+ const postDecimalString = roundingDownMatch[3];
46
+ const lastDigit = parseInt(postDecimalString.charAt(postDecimalString.length - 1), 10);
47
+ const postDecimal = parseInt(postDecimalString, 10);
48
+ const preDecimal = parseInt(roundingDownMatch[2], 10);
49
+
50
+ const origPostDecimalString = roundingDownMatch[3];
51
+
52
+ let newPostDecimal = (postDecimal + 10 - lastDigit).toString();
53
+ let carry = 0;
54
+ if (newPostDecimal.length > postDecimal.toString().length) {
55
+ // Left-shift
56
+ newPostDecimal = newPostDecimal.substring(1);
57
+ carry = 1;
58
+ }
59
+
60
+ // parseInt(...).toString() removes leading zeroes. Add them back.
61
+ while (newPostDecimal.length < origPostDecimalString.length) {
62
+ newPostDecimal = carry.toString(10) + newPostDecimal;
63
+ carry = 0;
64
+ }
65
+
66
+ text = `${negativeSign + (preDecimal + carry).toString()}.${newPostDecimal}`;
67
+ }
68
+
69
+ text = text.replace(fixRoundingUpExp, '$1');
70
+
71
+ return cleanUpNumber(text);
72
+ };
73
+
74
+ const numberExp = /^([-]?)(\d*)[.](\d+)$/;
75
+ export const getLenAfterDecimal = (numberAsString: string) => {
76
+ const numberMatch = numberExp.exec(numberAsString);
77
+ if (!numberMatch) {
78
+ // If not a match, either the number is exponential notation (or is something
79
+ // like NaN or Infinity)
80
+ if (numberAsString.search(/[eE]/) !== -1 || /^[a-zA-Z]+$/.exec(numberAsString)) {
81
+ return -1;
82
+ // Or it has no decimal point
83
+ } else {
84
+ return 0;
85
+ }
86
+ }
87
+
88
+ const afterDecimalLen = numberMatch[3].length;
89
+ return afterDecimalLen;
90
+ };
91
+
92
+ // [reference] should be a string representation of a base-10 number (no exponential (e.g. 10e10))
93
+ export const toStringOfSamePrecision = (num: number, ...references: string[]): string => {
94
+ const text = num.toString(10);
95
+ const textMatch = numberExp.exec(text);
96
+ if (!textMatch) {
97
+ return text;
98
+ }
99
+
100
+ let decimalPlaces = -1;
101
+ for (const reference of references) {
102
+ decimalPlaces = Math.max(getLenAfterDecimal(reference), decimalPlaces);
103
+ }
104
+
105
+ if (decimalPlaces === -1) {
106
+ return toRoundedString(num);
107
+ }
108
+
109
+ // Make text's after decimal length match [afterDecimalLen].
110
+ let postDecimal = textMatch[3].substring(0, decimalPlaces);
111
+ let preDecimal = textMatch[2];
112
+ const nextDigit = textMatch[3].charAt(decimalPlaces);
113
+
114
+ if (nextDigit !== '') {
115
+ const asNumber = parseInt(nextDigit, 10);
116
+ if (asNumber >= 5) {
117
+ // Don't attempt to parseInt() an empty string.
118
+ if (postDecimal.length > 0) {
119
+ const leadingZeroMatch = /^(0+)(\d*)$/.exec(postDecimal);
120
+
121
+ let leadingZeroes = '';
122
+ let postLeading = postDecimal;
123
+ if (leadingZeroMatch) {
124
+ leadingZeroes = leadingZeroMatch[1];
125
+ postLeading = leadingZeroMatch[2];
126
+ }
127
+
128
+ postDecimal = (parseInt(postDecimal) + 1).toString();
129
+
130
+ // If postDecimal got longer, remove leading zeroes if possible
131
+ if (postDecimal.length > postLeading.length && leadingZeroes.length > 0) {
132
+ leadingZeroes = leadingZeroes.substring(1);
133
+ }
134
+
135
+ postDecimal = leadingZeroes + postDecimal;
136
+ }
137
+
138
+ if (postDecimal.length === 0 || postDecimal.length > decimalPlaces) {
139
+ preDecimal = (parseInt(preDecimal) + 1).toString();
140
+ postDecimal = postDecimal.substring(1);
141
+ }
142
+ }
143
+ }
144
+
145
+ const negativeSign = textMatch[1];
146
+ return cleanUpNumber(`${negativeSign}${preDecimal}.${postDecimal}`);
147
+ };
@@ -1,9 +1,24 @@
1
+ /**
2
+ * Handles `HTMLCanvasElement`s (or other drawing surfaces if being used) used to display the editor's contents.
3
+ *
4
+ * @example
5
+ * ```
6
+ * const editor = new Editor(document.body);
7
+ * const w = editor.display.width;
8
+ * const h = editor.display.height;
9
+ * const center = Vec2.of(w / 2, h / 2);
10
+ * const colorAtCenter = editor.display.getColorAt(center);
11
+ * ```
12
+ *
13
+ * @packageDocumentation
14
+ */
15
+
1
16
  import AbstractRenderer from './renderers/AbstractRenderer';
2
17
  import CanvasRenderer from './renderers/CanvasRenderer';
3
18
  import { Editor } from '../Editor';
4
19
  import { EditorEventType } from '../types';
5
20
  import DummyRenderer from './renderers/DummyRenderer';
6
- import { Point2, Vec2 } from '../geometry/Vec2';
21
+ import { Point2, Vec2 } from '../math/Vec2';
7
22
  import RenderingCache from './caching/RenderingCache';
8
23
  import TextOnlyRenderer from './renderers/TextOnlyRenderer';
9
24
  import Color4 from '../Color4';
@@ -18,10 +33,12 @@ export default class Display {
18
33
  private dryInkRenderer: AbstractRenderer;
19
34
  private wetInkRenderer: AbstractRenderer;
20
35
  private textRenderer: TextOnlyRenderer;
36
+ private textRerenderOutput: HTMLElement|null = null;
21
37
  private cache: RenderingCache;
22
38
  private resizeSurfacesCallback?: ()=> void;
23
39
  private flattenCallback?: ()=> void;
24
40
 
41
+ /** @internal */
25
42
  public constructor(
26
43
  private editor: Editor, mode: RenderingMode, private parent: HTMLElement|null
27
44
  ) {
@@ -59,10 +76,10 @@ export default class Display {
59
76
  return this.dryInkRenderer.canRenderFromWithoutDataLoss(renderer);
60
77
  },
61
78
  blockResolution: cacheBlockResolution,
62
- cacheSize: 500 * 500 * 4 * 200,
79
+ cacheSize: 500 * 500 * 4 * 150,
63
80
  maxScale: 1.5,
64
- minComponentsPerCache: 50,
65
- minComponentsToUseCache: 120,
81
+ minComponentsPerCache: 45,
82
+ minComponentsToUseCache: 105,
66
83
  });
67
84
 
68
85
  this.editor.notifier.on(EditorEventType.DisplayResized, event => {
@@ -74,9 +91,11 @@ export default class Display {
74
91
  });
75
92
  }
76
93
 
77
- // Returns the visible width of the display (e.g. how much
78
- // space the display's element takes up in the x direction
79
- // in the DOM).
94
+ /**
95
+ * @returns the visible width of the display (e.g. how much
96
+ * space the display's element takes up in the x direction
97
+ * in the DOM).
98
+ */
80
99
  public get width(): number {
81
100
  return this.dryInkRenderer.displaySize().x;
82
101
  }
@@ -85,10 +104,15 @@ export default class Display {
85
104
  return this.dryInkRenderer.displaySize().y;
86
105
  }
87
106
 
107
+ /** @internal */
88
108
  public getCache() {
89
109
  return this.cache;
90
110
  }
91
111
 
112
+ /**
113
+ * @returns the color at the given point on the dry ink renderer, or `null` if `screenPos`
114
+ * is not on the display.
115
+ */
92
116
  public getColorAt = (_screenPos: Point2): Color4|null => {
93
117
  return null;
94
118
  };
@@ -158,20 +182,35 @@ export default class Display {
158
182
  rerenderButton.classList.add('rerenderButton');
159
183
  rerenderButton.innerText = this.editor.localization.rerenderAsText;
160
184
 
161
- const rerenderOutput = document.createElement('div');
162
- rerenderOutput.ariaLive = 'polite';
185
+ this.textRerenderOutput = document.createElement('div');
186
+ this.textRerenderOutput.setAttribute('aria-live', 'polite');
163
187
 
164
188
  rerenderButton.onclick = () => {
165
- this.textRenderer.clear();
166
- this.editor.image.render(this.textRenderer, this.editor.viewport);
167
- rerenderOutput.innerText = this.textRenderer.getDescription();
189
+ this.rerenderAsText();
168
190
  };
169
191
 
170
- textRendererOutputContainer.replaceChildren(rerenderButton, rerenderOutput);
192
+ textRendererOutputContainer.replaceChildren(rerenderButton, this.textRerenderOutput);
171
193
  this.editor.createHTMLOverlay(textRendererOutputContainer);
172
194
  }
173
195
 
174
- // Clears the drawing surfaces and otherwise prepares for a rerender.
196
+ /**
197
+ * Rerenders the text-based display.
198
+ * The text-based display is intended for screen readers and can be navigated to by pressing `tab`.
199
+ */
200
+ public rerenderAsText() {
201
+ this.textRenderer.clear();
202
+ this.editor.image.render(this.textRenderer, this.editor.viewport);
203
+
204
+ if (this.textRerenderOutput) {
205
+ this.textRerenderOutput.innerText = this.textRenderer.getDescription();
206
+ }
207
+ }
208
+
209
+ /**
210
+ * Clears the drawing surfaces and otherwise prepares for a rerender.
211
+ *
212
+ * @returns the dry ink renderer.
213
+ */
175
214
  public startRerender(): AbstractRenderer {
176
215
  this.resizeSurfacesCallback?.();
177
216
  this.wetInkRenderer.clear();
@@ -180,19 +219,28 @@ export default class Display {
180
219
  return this.dryInkRenderer;
181
220
  }
182
221
 
222
+ /**
223
+ * If `draftMode`, the dry ink renderer is configured to render
224
+ * low-quality output.
225
+ */
183
226
  public setDraftMode(draftMode: boolean) {
184
227
  this.dryInkRenderer.setDraftMode(draftMode);
185
228
  }
186
229
 
230
+ /** @internal */
187
231
  public getDryInkRenderer(): AbstractRenderer {
188
232
  return this.dryInkRenderer;
189
233
  }
190
234
 
235
+ /**
236
+ * @returns The renderer used for showing action previews (e.g. an unfinished stroke).
237
+ * The `wetInkRenderer`'s surface is stacked above the `dryInkRenderer`'s.
238
+ */
191
239
  public getWetInkRenderer(): AbstractRenderer {
192
240
  return this.wetInkRenderer;
193
241
  }
194
242
 
195
- // Re-renders the contents of the wetInkRenderer onto the dryInkRenderer
243
+ /** Re-renders the contents of the wetInkRenderer onto the dryInkRenderer. */
196
244
  public flatten() {
197
245
  this.flattenCallback?.();
198
246
  }
@@ -1,7 +1,7 @@
1
1
  /* @jest-environment jsdom */
2
2
 
3
- import Rect2 from '../../geometry/Rect2';
4
- import { Vec2 } from '../../geometry/Vec2';
3
+ import Rect2 from '../../math/Rect2';
4
+ import { Vec2 } from '../../math/Vec2';
5
5
  import CacheRecord from './CacheRecord';
6
6
  import { createCache } from './testUtils';
7
7
 
@@ -11,7 +11,7 @@ describe('CacheRecord', () => {
11
11
  const { cache } = createCache(undefined, {
12
12
  blockResolution,
13
13
  });
14
- const state = cache.getSharedState();
14
+ const state = cache['sharedState'];
15
15
 
16
16
  const record = new CacheRecord(() => {}, state);
17
17
 
@@ -1,5 +1,5 @@
1
- import Mat33 from '../../geometry/Mat33';
2
- import Rect2 from '../../geometry/Rect2';
1
+ import Mat33 from '../../math/Mat33';
2
+ import Rect2 from '../../math/Rect2';
3
3
  import AbstractRenderer from '../renderers/AbstractRenderer';
4
4
  import { BeforeDeallocCallback, CacheState } from './types';
5
5
 
@@ -11,6 +11,9 @@ export default class CacheRecord {
11
11
  private lastUsedCycle: number;
12
12
  private allocd: boolean = false;
13
13
 
14
+ // For debugging
15
+ public allocCount: number = 0;
16
+
14
17
  public constructor(
15
18
  private onBeforeDeallocCallback: BeforeDeallocCallback|null,
16
19
  private cacheState: CacheState,
@@ -46,6 +49,7 @@ export default class CacheRecord {
46
49
  this.allocd = true;
47
50
  this.onBeforeDeallocCallback = newDeallocCallback;
48
51
  this.lastUsedCycle = this.cacheState.currentRenderingCycle;
52
+ this.allocCount ++;
49
53
  }
50
54
 
51
55
  public getLastUsedCycle(): number {
@@ -1,38 +1,64 @@
1
- import { BeforeDeallocCallback, PartialCacheState } from './types';
1
+ import { BeforeDeallocCallback, CacheProps, CacheState } from './types';
2
2
  import CacheRecord from './CacheRecord';
3
- import Rect2 from '../../geometry/Rect2';
3
+ import Rect2 from '../../math/Rect2';
4
4
 
5
+ const debugMode = false;
5
6
 
6
7
  export class CacheRecordManager {
7
8
  // Fixed-size array: Cache blocks are assigned indicies into [cachedCanvases].
8
9
  private cacheRecords: CacheRecord[] = [];
9
10
  private maxCanvases: number;
11
+ private cacheState: CacheState;
10
12
 
11
- public constructor(private readonly cacheState: PartialCacheState) {
12
- const cacheProps = cacheState.props;
13
+ public constructor(cacheProps: CacheProps) {
13
14
  this.maxCanvases = Math.ceil(
14
15
  // Assuming four components per pixel:
15
16
  cacheProps.cacheSize / 4 / cacheProps.blockResolution.x / cacheProps.blockResolution.y
16
17
  );
17
18
  }
18
19
 
20
+ public setSharedState(state: CacheState) {
21
+ this.cacheState = state;
22
+ }
23
+
19
24
  public allocCanvas(drawTo: Rect2, onDealloc: BeforeDeallocCallback): CacheRecord {
20
25
  if (this.cacheRecords.length < this.maxCanvases) {
21
26
  const record: CacheRecord = new CacheRecord(
22
27
  onDealloc,
23
- {
24
- ...this.cacheState,
25
- recordManager: this,
26
- },
28
+ this.cacheState,
27
29
  );
28
30
  record.setRenderingRegion(drawTo);
29
31
  this.cacheRecords.push(record);
30
32
 
33
+ if (debugMode) {
34
+ console.log('[Cache] Cache spaces used: ', this.cacheRecords.length, ' of ', this.maxCanvases);
35
+ }
36
+
31
37
  return record;
32
38
  } else {
33
39
  const lru = this.getLeastRecentlyUsedRecord()!;
40
+
41
+ if (debugMode) {
42
+ console.log(
43
+ '[Cache] Re-alloc. Times allocated: ', lru.allocCount,
44
+ '\nLast used cycle: ', lru.getLastUsedCycle(),
45
+ '\nCurrent cycle: ', this.cacheState.currentRenderingCycle
46
+ );
47
+ }
48
+
34
49
  lru.realloc(onDealloc);
35
50
  lru.setRenderingRegion(drawTo);
51
+
52
+ if (debugMode) {
53
+ console.log(
54
+ '[Cache] Now re-alloc\'d. Last used cycle: ', lru.getLastUsedCycle()
55
+ );
56
+ console.assert(
57
+ lru['cacheState'] === this.cacheState,
58
+ '[Cache] Unequal cache states! cacheState should be a shared object!'
59
+ );
60
+ }
61
+
36
62
  return lru;
37
63
  }
38
64
  }
@@ -3,11 +3,11 @@
3
3
  import DummyRenderer from '../renderers/DummyRenderer';
4
4
  import { createCache } from './testUtils';
5
5
  import Stroke from '../../components/Stroke';
6
- import Path from '../../geometry/Path';
6
+ import Path from '../../math/Path';
7
7
  import Color4 from '../../Color4';
8
8
  import EditorImage from '../../EditorImage';
9
9
  import Viewport from '../../Viewport';
10
- import Mat33 from '../../geometry/Mat33';
10
+ import Mat33 from '../../math/Mat33';
11
11
 
12
12
  describe('RenderingCache', () => {
13
13
  const testPath = Path.fromString('M0,0 l100,500 l-20,20 L-100,-100');
@@ -35,7 +35,7 @@ describe('RenderingCache', () => {
35
35
  expect(lastRenderer).not.toBeNull();
36
36
  expect(lastRenderer!.renderedPathCount).toBe(1);
37
37
 
38
- editor.dispatch(new Viewport.ViewportTransform(Mat33.scaling2D(0.1)));
38
+ editor.dispatch(Viewport.transformBy(Mat33.scaling2D(0.1)));
39
39
  editor.image.renderWithCache(screenRenderer, cache, editor.viewport);
40
40
  expect(allocdRenderers).toBe(1);
41
41
  expect(lastRenderer!.renderedPathCount).toBe(1);
@@ -1,49 +1,44 @@
1
1
  import { ImageNode } from '../../EditorImage';
2
- import Rect2 from '../../geometry/Rect2';
2
+ import Rect2 from '../../math/Rect2';
3
3
  import Viewport from '../../Viewport';
4
4
  import AbstractRenderer from '../renderers/AbstractRenderer';
5
5
  import RenderingCacheNode from './RenderingCacheNode';
6
6
  import { CacheRecordManager } from './CacheRecordManager';
7
- import { CacheProps, CacheState, PartialCacheState } from './types';
7
+ import { CacheProps, CacheState } from './types';
8
8
 
9
9
  export default class RenderingCache {
10
- private partialSharedState: PartialCacheState;
10
+ private sharedState: CacheState;
11
11
  private recordManager: CacheRecordManager;
12
12
  private rootNode: RenderingCacheNode|null;
13
13
 
14
14
  public constructor(cacheProps: CacheProps) {
15
- this.partialSharedState = {
15
+ this.recordManager = new CacheRecordManager(cacheProps);
16
+ this.sharedState = {
16
17
  props: cacheProps,
17
18
  currentRenderingCycle: 0,
18
- };
19
- this.recordManager = new CacheRecordManager(this.partialSharedState);
20
- }
21
-
22
- public getSharedState(): CacheState {
23
- return {
24
- ...this.partialSharedState,
25
19
  recordManager: this.recordManager,
26
20
  };
21
+ this.recordManager.setSharedState(this.sharedState);
27
22
  }
28
23
 
29
24
  public render(screenRenderer: AbstractRenderer, image: ImageNode, viewport: Viewport) {
30
25
  const visibleRect = viewport.visibleRect;
31
- this.partialSharedState.currentRenderingCycle ++;
26
+ this.sharedState.currentRenderingCycle ++;
32
27
 
33
28
  // If we can't use the cache,
34
- if (!this.partialSharedState.props.isOfCorrectType(screenRenderer)) {
29
+ if (!this.sharedState.props.isOfCorrectType(screenRenderer)) {
35
30
  image.render(screenRenderer, visibleRect);
36
31
  return;
37
32
  }
38
33
 
39
34
  if (!this.rootNode) {
40
35
  // Adjust the node so that it has the correct aspect ratio
41
- const res = this.partialSharedState.props.blockResolution;
36
+ const res = this.sharedState.props.blockResolution;
42
37
 
43
38
  const topLeft = visibleRect.topLeft;
44
39
  this.rootNode = new RenderingCacheNode(
45
40
  new Rect2(topLeft.x, topLeft.y, res.x, res.y),
46
- this.getSharedState()
41
+ this.sharedState
47
42
  );
48
43
  }
49
44
 
@@ -54,7 +49,7 @@ export default class RenderingCache {
54
49
  this.rootNode = this.rootNode!.smallestChildContaining(visibleRect) ?? this.rootNode;
55
50
 
56
51
  const visibleLeaves = image.getLeavesIntersectingRegion(viewport.visibleRect, rect => screenRenderer.isTooSmallToRender(rect));
57
- if (visibleLeaves.length > this.partialSharedState.props.minComponentsToUseCache) {
52
+ if (visibleLeaves.length > this.sharedState.props.minComponentsToUseCache) {
58
53
  this.rootNode!.renderItems(screenRenderer, [ image ], viewport);
59
54
  } else {
60
55
  image.render(screenRenderer, visibleRect);
@@ -3,7 +3,7 @@
3
3
 
4
4
  import Color4 from '../../Color4';
5
5
  import { ImageNode, sortLeavesByZIndex } from '../../EditorImage';
6
- import Rect2 from '../../geometry/Rect2';
6
+ import Rect2 from '../../math/Rect2';
7
7
  import Viewport from '../../Viewport';
8
8
  import AbstractRenderer from '../renderers/AbstractRenderer';
9
9
  import CacheRecord from './CacheRecord';
@@ -151,12 +151,15 @@ export default class RenderingCacheNode {
151
151
  return result;
152
152
  }
153
153
 
154
- private renderingIsUpToDate(sortedIds: number[]) {
155
- if (this.cachedRenderer === null || sortedIds.length !== this.renderedIds.length) {
154
+ // Returns true iff all elems of this.renderedIds are in sortedIds.
155
+ // sortedIds should be sorted by z-index (or some other order, so long as they are
156
+ // sorted by the same thing as this.renderedIds.)
157
+ private allRenderedIdsIn(sortedIds: number[]) {
158
+ if (this.renderedIds.length > sortedIds.length) {
156
159
  return false;
157
160
  }
158
161
 
159
- for (let i = 0; i < sortedIds.length; i++) {
162
+ for (let i = 0; i < this.renderedIds.length; i++) {
160
163
  if (sortedIds[i] !== this.renderedIds[i]) {
161
164
  return false;
162
165
  }
@@ -165,6 +168,14 @@ export default class RenderingCacheNode {
165
168
  return true;
166
169
  }
167
170
 
171
+ private renderingIsUpToDate(sortedIds: number[]) {
172
+ if (this.cachedRenderer === null || sortedIds.length !== this.renderedIds.length) {
173
+ return false;
174
+ }
175
+
176
+ return this.allRenderedIdsIn(sortedIds);
177
+ }
178
+
168
179
  // Render all [items] within [viewport]
169
180
  public renderItems(screenRenderer: AbstractRenderer, items: ImageNode[], viewport: Viewport) {
170
181
  if (
@@ -238,8 +249,7 @@ export default class RenderingCacheNode {
238
249
  }
239
250
 
240
251
  // Is it worth it to render the items?
241
- // TODO: Replace this with something performace based.
242
- // TODO: Determine whether it is 'worth it' to cache this depending on rendering time.
252
+ // TODO: Consider replacing this with something performace based.
243
253
  if (leavesByIds.length > this.cacheState.props.minComponentsPerCache) {
244
254
  let fullRerenderNeeded = true;
245
255
  if (!this.cachedRenderer) {
@@ -247,7 +257,7 @@ export default class RenderingCacheNode {
247
257
  this.region,
248
258
  () => this.onRegionDealloc()
249
259
  );
250
- } else if (leavesByIds.length > this.renderedIds.length && this.renderedMaxZIndex !== null) {
260
+ } else if (leavesByIds.length > this.renderedIds.length && this.allRenderedIdsIn(leafIds) && this.renderedMaxZIndex !== null) {
251
261
  // We often don't need to do a full re-render even if something's changed.
252
262
  // Check whether we can just draw on top of the existing cache.
253
263
  const newLeaves = [];
@@ -287,6 +297,12 @@ export default class RenderingCacheNode {
287
297
  screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: Color4.clay });
288
298
  }
289
299
  }
300
+ } else if (debugMode) {
301
+ console.log('Decided on a full re-render. Reason: At least one of the following is false:',
302
+ '\n leafIds.length > this.renderedIds.length: ', leafIds.length > this.renderedIds.length,
303
+ '\n this.allRenderedIdsIn(leafIds): ', this.allRenderedIdsIn(leafIds),
304
+ '\n this.renderedMaxZIndex !== null: ', this.renderedMaxZIndex !== null,
305
+ '\n\nthis.rerenderedIds: ', this.renderedIds, ', leafIds: ', leafIds);
290
306
  }
291
307
 
292
308
  if (fullRerenderNeeded) {
@@ -1,4 +1,4 @@
1
- import { Vec2 } from '../../geometry/Vec2';
1
+ import { Vec2 } from '../../math/Vec2';
2
2
  import DummyRenderer from '../renderers/DummyRenderer';
3
3
  import createEditor from '../../testing/createEditor';
4
4
  import AbstractRenderer from '../renderers/AbstractRenderer';
@@ -1,4 +1,4 @@
1
- import { Vec2 } from '../../geometry/Vec2';
1
+ import { Vec2 } from '../../math/Vec2';
2
2
  import AbstractRenderer from '../renderers/AbstractRenderer';
3
3
  import { CacheRecordManager } from './CacheRecordManager';
4
4
 
@@ -27,13 +27,8 @@ export interface CacheProps {
27
27
  minComponentsToUseCache: number;
28
28
  }
29
29
 
30
- // CacheRecordManager relies on a partial copy of the shared state. Thus,
31
- // we need to separate partial/non-partial state.
32
- export interface PartialCacheState {
30
+ export interface CacheState {
33
31
  currentRenderingCycle: number;
34
32
  props: CacheProps;
35
- }
36
-
37
- export interface CacheState extends PartialCacheState {
38
33
  recordManager: CacheRecordManager;
39
34
  }
@@ -1,10 +1,14 @@
1
1
 
2
2
  export interface TextRendererLocalization {
3
+ pathNodeCount(pathCount: number): string;
4
+ textNodeCount(nodeCount: number): string;
3
5
  textNode(content: string): string;
4
6
  rerenderAsText: string;
5
7
  }
6
8
 
7
9
  export const defaultTextRendererLocalization: TextRendererLocalization = {
10
+ pathNodeCount: (count: number) => `There are ${count} visible path objects.`,
11
+ textNodeCount: (count: number) => `There are ${count} visible text nodes.`,
8
12
  textNode: (content: string) => `Text: ${content}`,
9
13
  rerenderAsText: 'Re-render as text',
10
14
  };
@@ -1,9 +1,9 @@
1
1
  import { LoadSaveDataTable } from '../../components/AbstractComponent';
2
2
  import { TextStyle } from '../../components/Text';
3
- import Mat33 from '../../geometry/Mat33';
4
- import Path, { PathCommand, PathCommandType } from '../../geometry/Path';
5
- import Rect2 from '../../geometry/Rect2';
6
- import { Point2, Vec2 } from '../../geometry/Vec2';
3
+ import Mat33 from '../../math/Mat33';
4
+ import Path, { PathCommand, PathCommandType } from '../../math/Path';
5
+ import Rect2 from '../../math/Rect2';
6
+ import { Point2, Vec2 } from '../../math/Vec2';
7
7
  import Viewport from '../../Viewport';
8
8
  import RenderingStyle, { stylesEqual } from '../RenderingStyle';
9
9
 
@@ -1,9 +1,9 @@
1
1
  import Color4 from '../../Color4';
2
2
  import Text, { TextStyle } from '../../components/Text';
3
- import Mat33 from '../../geometry/Mat33';
4
- import Rect2 from '../../geometry/Rect2';
5
- import { Point2, Vec2 } from '../../geometry/Vec2';
6
- import Vec3 from '../../geometry/Vec3';
3
+ import Mat33 from '../../math/Mat33';
4
+ import Rect2 from '../../math/Rect2';
5
+ import { Point2, Vec2 } from '../../math/Vec2';
6
+ import Vec3 from '../../math/Vec3';
7
7
  import Viewport from '../../Viewport';
8
8
  import RenderingStyle from '../RenderingStyle';
9
9
  import AbstractRenderer, { RenderablePathSpec } from './AbstractRenderer';
@@ -64,7 +64,7 @@ export default class CanvasRenderer extends AbstractRenderer {
64
64
  this.minRenderSizeAnyDimen = 2;
65
65
  } else {
66
66
  this.minSquareCurveApproxDist = 1;
67
- this.minRenderSizeBothDimens = 1;
67
+ this.minRenderSizeBothDimens = 0.5;
68
68
  this.minRenderSizeAnyDimen = 0;
69
69
  }
70
70
  }