js-draw 0.0.10 → 0.1.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 (95) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/dist/bundle.js +1 -1
  3. package/dist/src/Editor.d.ts +2 -2
  4. package/dist/src/Editor.js +13 -7
  5. package/dist/src/EditorImage.d.ts +15 -7
  6. package/dist/src/EditorImage.js +41 -35
  7. package/dist/src/SVGLoader.d.ts +3 -2
  8. package/dist/src/SVGLoader.js +9 -7
  9. package/dist/src/Viewport.d.ts +4 -0
  10. package/dist/src/Viewport.js +41 -0
  11. package/dist/src/components/AbstractComponent.d.ts +3 -2
  12. package/dist/src/components/AbstractComponent.js +3 -0
  13. package/dist/src/components/SVGGlobalAttributesObject.d.ts +1 -1
  14. package/dist/src/components/SVGGlobalAttributesObject.js +1 -1
  15. package/dist/src/components/Stroke.d.ts +1 -1
  16. package/dist/src/components/UnknownSVGObject.d.ts +1 -1
  17. package/dist/src/components/UnknownSVGObject.js +1 -1
  18. package/dist/src/components/builders/ArrowBuilder.d.ts +1 -1
  19. package/dist/src/components/builders/FreehandLineBuilder.d.ts +1 -1
  20. package/dist/src/components/builders/LineBuilder.d.ts +1 -1
  21. package/dist/src/components/builders/RectangleBuilder.d.ts +1 -1
  22. package/dist/src/components/builders/types.d.ts +1 -1
  23. package/dist/src/geometry/Mat33.js +3 -0
  24. package/dist/src/geometry/Path.d.ts +1 -1
  25. package/dist/src/geometry/Path.js +5 -3
  26. package/dist/src/geometry/Rect2.d.ts +1 -0
  27. package/dist/src/geometry/Rect2.js +28 -1
  28. package/dist/src/{Display.d.ts → rendering/Display.d.ts} +5 -2
  29. package/dist/src/{Display.js → rendering/Display.js} +33 -4
  30. package/dist/src/rendering/caching/CacheRecord.d.ts +19 -0
  31. package/dist/src/rendering/caching/CacheRecord.js +51 -0
  32. package/dist/src/rendering/caching/CacheRecordManager.d.ts +11 -0
  33. package/dist/src/rendering/caching/CacheRecordManager.js +39 -0
  34. package/dist/src/rendering/caching/RenderingCache.d.ts +12 -0
  35. package/dist/src/rendering/caching/RenderingCache.js +36 -0
  36. package/dist/src/rendering/caching/RenderingCacheNode.d.ts +28 -0
  37. package/dist/src/rendering/caching/RenderingCacheNode.js +294 -0
  38. package/dist/src/rendering/caching/testUtils.d.ts +9 -0
  39. package/dist/src/rendering/caching/testUtils.js +20 -0
  40. package/dist/src/rendering/caching/types.d.ts +20 -0
  41. package/dist/src/rendering/caching/types.js +1 -0
  42. package/dist/src/rendering/{AbstractRenderer.d.ts → renderers/AbstractRenderer.d.ts} +18 -8
  43. package/dist/src/rendering/{AbstractRenderer.js → renderers/AbstractRenderer.js} +36 -2
  44. package/dist/src/rendering/{CanvasRenderer.d.ts → renderers/CanvasRenderer.d.ts} +10 -5
  45. package/dist/src/rendering/{CanvasRenderer.js → renderers/CanvasRenderer.js} +60 -20
  46. package/dist/src/rendering/{DummyRenderer.d.ts → renderers/DummyRenderer.d.ts} +9 -5
  47. package/dist/src/rendering/{DummyRenderer.js → renderers/DummyRenderer.js} +35 -4
  48. package/dist/src/rendering/{SVGRenderer.d.ts → renderers/SVGRenderer.d.ts} +4 -3
  49. package/dist/src/rendering/{SVGRenderer.js → renderers/SVGRenderer.js} +14 -11
  50. package/dist/src/testing/createEditor.js +1 -1
  51. package/dist/src/toolbar/HTMLToolbar.js +1 -1
  52. package/dist/src/tools/SelectionTool.js +9 -24
  53. package/dist/src/types.d.ts +2 -1
  54. package/package.json +1 -1
  55. package/src/Editor.ts +15 -8
  56. package/src/EditorImage.test.ts +2 -2
  57. package/src/EditorImage.ts +53 -41
  58. package/src/SVGLoader.ts +11 -8
  59. package/src/Viewport.ts +56 -0
  60. package/src/components/AbstractComponent.ts +6 -2
  61. package/src/components/SVGGlobalAttributesObject.ts +2 -2
  62. package/src/components/Stroke.ts +1 -1
  63. package/src/components/UnknownSVGObject.ts +2 -2
  64. package/src/components/builders/ArrowBuilder.ts +1 -1
  65. package/src/components/builders/FreehandLineBuilder.ts +1 -1
  66. package/src/components/builders/LineBuilder.ts +1 -1
  67. package/src/components/builders/RectangleBuilder.ts +1 -1
  68. package/src/components/builders/types.ts +1 -1
  69. package/src/geometry/Mat33.ts +3 -0
  70. package/src/geometry/Path.toString.test.ts +12 -2
  71. package/src/geometry/Path.ts +8 -4
  72. package/src/geometry/Rect2.test.ts +38 -8
  73. package/src/geometry/Rect2.ts +32 -1
  74. package/src/{Display.ts → rendering/Display.ts} +38 -6
  75. package/src/rendering/caching/CacheRecord.test.ts +49 -0
  76. package/src/rendering/caching/CacheRecord.ts +72 -0
  77. package/src/rendering/caching/CacheRecordManager.ts +55 -0
  78. package/src/rendering/caching/RenderingCache.test.ts +44 -0
  79. package/src/rendering/caching/RenderingCache.ts +56 -0
  80. package/src/rendering/caching/RenderingCacheNode.ts +365 -0
  81. package/src/rendering/caching/testUtils.ts +34 -0
  82. package/src/rendering/caching/types.ts +35 -0
  83. package/src/rendering/{AbstractRenderer.ts → renderers/AbstractRenderer.ts} +55 -8
  84. package/src/rendering/{CanvasRenderer.ts → renderers/CanvasRenderer.ts} +74 -25
  85. package/src/rendering/renderers/DummyRenderer.test.ts +43 -0
  86. package/src/rendering/{DummyRenderer.ts → renderers/DummyRenderer.ts} +50 -7
  87. package/src/rendering/{SVGRenderer.ts → renderers/SVGRenderer.ts} +17 -13
  88. package/src/testing/createEditor.ts +1 -1
  89. package/src/toolbar/HTMLToolbar.ts +1 -1
  90. package/src/tools/SelectionTool.test.ts +1 -1
  91. package/src/tools/SelectionTool.ts +12 -33
  92. package/src/types.ts +10 -3
  93. package/tsconfig.json +1 -0
  94. package/dist/__mocks__/coloris.d.ts +0 -2
  95. package/dist/__mocks__/coloris.js +0 -5
@@ -1,10 +1,15 @@
1
1
  import Editor from './Editor';
2
- import AbstractRenderer from './rendering/AbstractRenderer';
2
+ import AbstractRenderer from './rendering/renderers/AbstractRenderer';
3
3
  import Command from './commands/Command';
4
4
  import Viewport from './Viewport';
5
5
  import AbstractComponent from './components/AbstractComponent';
6
6
  import Rect2 from './geometry/Rect2';
7
7
  import { EditorLocalization } from './localization';
8
+ import RenderingCache from './rendering/caching/RenderingCache';
9
+
10
+ export const sortLeavesByZIndex = (leaves: Array<ImageNode>) => {
11
+ leaves.sort((a, b) => a.getContent()!.getZIndex() - b.getContent()!.getZIndex());
12
+ };
8
13
 
9
14
  // Handles lookup/storage of elements in the image
10
15
  export default class EditorImage {
@@ -20,7 +25,7 @@ export default class EditorImage {
20
25
 
21
26
  // Returns the parent of the given element, if it exists.
22
27
  public findParent(elem: AbstractComponent): ImageNode|null {
23
- const candidates = this.root.getLeavesInRegion(elem.getBBox());
28
+ const candidates = this.root.getLeavesIntersectingRegion(elem.getBBox());
24
29
  for (const candidate of candidates) {
25
30
  if (candidate.getContent() === elem) {
26
31
  return candidate;
@@ -29,25 +34,18 @@ export default class EditorImage {
29
34
  return null;
30
35
  }
31
36
 
32
- private sortLeaves(leaves: ImageNode[]) {
33
- leaves.sort((a, b) => a.getContent()!.zIndex - b.getContent()!.zIndex);
37
+ public renderWithCache(screenRenderer: AbstractRenderer, cache: RenderingCache, viewport: Viewport) {
38
+ cache.render(screenRenderer, this.root, viewport);
34
39
  }
35
40
 
36
- public render(renderer: AbstractRenderer, viewport: Viewport, minFraction: number = 0.001) {
37
- // Don't render components that are < 0.1% of the viewport.
38
- const leaves = this.root.getLeavesInRegion(viewport.visibleRect, minFraction);
39
- this.sortLeaves(leaves);
40
-
41
- for (const leaf of leaves) {
42
- // Leaves by definition have content
43
- leaf.getContent()!.render(renderer, viewport.visibleRect);
44
- }
41
+ public render(renderer: AbstractRenderer, viewport: Viewport) {
42
+ this.root.render(renderer, viewport.visibleRect);
45
43
  }
46
44
 
47
45
  // Renders all nodes, even ones not within the viewport
48
46
  public renderAll(renderer: AbstractRenderer) {
49
47
  const leaves = this.root.getLeaves();
50
- this.sortLeaves(leaves);
48
+ sortLeavesByZIndex(leaves);
51
49
 
52
50
  for (const leaf of leaves) {
53
51
  leaf.getContent()!.render(renderer, leaf.getBBox());
@@ -55,8 +53,9 @@ export default class EditorImage {
55
53
  }
56
54
 
57
55
  public getElementsIntersectingRegion(region: Rect2): AbstractComponent[] {
58
- const leaves = this.root.getLeavesInRegion(region);
59
- this.sortLeaves(leaves);
56
+ const leaves = this.root.getLeavesIntersectingRegion(region);
57
+ sortLeavesByZIndex(leaves);
58
+
60
59
  return leaves.map(leaf => leaf.getContent()!);
61
60
  }
62
61
 
@@ -100,15 +99,17 @@ export default class EditorImage {
100
99
  }
101
100
 
102
101
  export type AddElementCommand = typeof EditorImage.AddElementCommand.prototype;
102
+ type TooSmallToRenderCheck = (rect: Rect2)=> boolean;
103
103
 
104
-
104
+ // TODO: Assign leaf nodes to CacheNodes. When leaf nodes are modified, the corresponding CacheNodes can be updated.
105
105
  export class ImageNode {
106
106
  private content: AbstractComponent|null;
107
107
  private bbox: Rect2;
108
108
  private children: ImageNode[];
109
109
  private targetChildCount: number = 30;
110
- private minZIndex: number|null;
111
- private maxZIndex: number|null;
110
+
111
+ private id: number;
112
+ private static idCounter: number = 0;
112
113
 
113
114
  public constructor(
114
115
  private parent: ImageNode|null = null
@@ -117,8 +118,15 @@ export class ImageNode {
117
118
  this.bbox = Rect2.empty;
118
119
  this.content = null;
119
120
 
120
- this.minZIndex = null;
121
- this.maxZIndex = null;
121
+ this.id = ImageNode.idCounter++;
122
+ }
123
+
124
+ public getId() {
125
+ return this.id;
126
+ }
127
+
128
+ public onContentChange() {
129
+ this.id = ImageNode.idCounter++;
122
130
  }
123
131
 
124
132
  public getContent(): AbstractComponent|null {
@@ -129,18 +137,25 @@ export class ImageNode {
129
137
  return this.parent;
130
138
  }
131
139
 
132
- private getChildrenInRegion(region: Rect2): ImageNode[] {
140
+ public getChildrenInRegion(region: Rect2): ImageNode[] {
133
141
  return this.children.filter(child => {
134
142
  return child.getBBox().intersects(region);
135
143
  });
136
144
  }
137
145
 
146
+ public getChildrenOrSelfIntersectingRegion(region: Rect2): ImageNode[] {
147
+ if (this.content) {
148
+ return [this];
149
+ }
150
+ return this.getChildrenInRegion(region);
151
+ }
152
+
138
153
  // Returns a list of `ImageNode`s with content (and thus no children).
139
- public getLeavesInRegion(region: Rect2, minFractionOfRegion: number = 0): ImageNode[] {
154
+ public getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[] {
140
155
  const result: ImageNode[] = [];
141
156
 
142
157
  // Don't render if too small
143
- if (this.bbox.maxDimension / region.maxDimension <= minFractionOfRegion) {
158
+ if (isTooSmall?.(this.bbox)) {
144
159
  return [];
145
160
  }
146
161
 
@@ -150,7 +165,7 @@ export class ImageNode {
150
165
 
151
166
  const children = this.getChildrenInRegion(region);
152
167
  for (const child of children) {
153
- result.push(...child.getLeavesInRegion(region, minFractionOfRegion));
168
+ result.push(...child.getLeavesIntersectingRegion(region, isTooSmall));
154
169
  }
155
170
 
156
171
  return result;
@@ -172,6 +187,8 @@ export class ImageNode {
172
187
  }
173
188
 
174
189
  public addLeaf(leaf: AbstractComponent): ImageNode {
190
+ this.onContentChange();
191
+
175
192
  if (this.content === null && this.children.length === 0) {
176
193
  this.content = leaf;
177
194
  this.recomputeBBox(true);
@@ -239,12 +256,8 @@ export class ImageNode {
239
256
  const oldBBox = this.bbox;
240
257
  if (this.content !== null) {
241
258
  this.bbox = this.content.getBBox();
242
- this.minZIndex = this.content.zIndex;
243
- this.maxZIndex = this.content.zIndex;
244
259
  } else {
245
260
  this.bbox = Rect2.empty;
246
- this.minZIndex = null;
247
- this.maxZIndex = null;
248
261
  let isFirst = true;
249
262
 
250
263
  for (const child of this.children) {
@@ -254,15 +267,6 @@ export class ImageNode {
254
267
  } else {
255
268
  this.bbox = this.bbox.union(child.getBBox());
256
269
  }
257
-
258
- this.minZIndex ??= child.minZIndex;
259
- this.maxZIndex ??= child.maxZIndex;
260
- if (child.minZIndex !== null && this.minZIndex !== null) {
261
- this.minZIndex = Math.min(child.minZIndex, this.minZIndex);
262
- }
263
- if (child.maxZIndex !== null && this.maxZIndex !== null) {
264
- this.maxZIndex = Math.max(child.maxZIndex, this.maxZIndex);
265
- }
266
270
  }
267
271
  }
268
272
 
@@ -295,9 +299,6 @@ export class ImageNode {
295
299
 
296
300
  // Remove this node and all of its children
297
301
  public remove() {
298
- this.minZIndex = null;
299
- this.maxZIndex = null;
300
-
301
302
  if (!this.parent) {
302
303
  this.content = null;
303
304
  this.children = [];
@@ -322,4 +323,15 @@ export class ImageNode {
322
323
  this.parent = null;
323
324
  this.children = [];
324
325
  }
326
+
327
+ public render(renderer: AbstractRenderer, visibleRect: Rect2) {
328
+ // Don't render components that are < 0.1% of the viewport.
329
+ const leaves = this.getLeavesIntersectingRegion(visibleRect, rect => renderer.isTooSmallToRender(rect));
330
+ sortLeavesByZIndex(leaves);
331
+
332
+ for (const leaf of leaves) {
333
+ // Leaves by definition have content
334
+ leaf.getContent()!.render(renderer, visibleRect);
335
+ }
336
+ }
325
337
  }
package/src/SVGLoader.ts CHANGED
@@ -5,8 +5,8 @@ import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject';
5
5
  import UnknownSVGObject from './components/UnknownSVGObject';
6
6
  import Path from './geometry/Path';
7
7
  import Rect2 from './geometry/Rect2';
8
- import { RenderablePathSpec, RenderingStyle } from './rendering/AbstractRenderer';
9
- import { ComponentAddedListener, ImageLoader, OnProgressListener } from './types';
8
+ import { RenderablePathSpec, RenderingStyle } from './rendering/renderers/AbstractRenderer';
9
+ import { ComponentAddedListener, ImageLoader, OnDetermineExportRectListener, OnProgressListener } from './types';
10
10
 
11
11
  type OnFinishListener = ()=> void;
12
12
 
@@ -16,6 +16,8 @@ export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
16
16
  export default class SVGLoader implements ImageLoader {
17
17
  private onAddComponent: ComponentAddedListener|null = null;
18
18
  private onProgress: OnProgressListener|null = null;
19
+ private onDetermineExportRect: OnDetermineExportRectListener|null = null;
20
+
19
21
  private processedCount: number = 0;
20
22
  private totalToProcess: number = 0;
21
23
  private rootViewBox: Rect2|null;
@@ -126,6 +128,7 @@ export default class SVGLoader implements ImageLoader {
126
128
  }
127
129
 
128
130
  this.rootViewBox = new Rect2(x, y, width, height);
131
+ this.onDetermineExportRect?.(this.rootViewBox);
129
132
  }
130
133
 
131
134
  private updateSVGAttrs(node: SVGSVGElement) {
@@ -172,10 +175,12 @@ export default class SVGLoader implements ImageLoader {
172
175
  }
173
176
 
174
177
  public async start(
175
- onAddComponent: ComponentAddedListener, onProgress: OnProgressListener
176
- ): Promise<Rect2> {
178
+ onAddComponent: ComponentAddedListener, onProgress: OnProgressListener,
179
+ onDetermineExportRect: OnDetermineExportRectListener|null = null
180
+ ): Promise<void> {
177
181
  this.onAddComponent = onAddComponent;
178
182
  this.onProgress = onProgress;
183
+ this.onDetermineExportRect = onDetermineExportRect;
179
184
 
180
185
  // Estimate the number of tags to process.
181
186
  this.totalToProcess = this.source.childElementCount;
@@ -185,14 +190,12 @@ export default class SVGLoader implements ImageLoader {
185
190
  await this.visit(this.source);
186
191
 
187
192
  const viewBox = this.rootViewBox;
188
- let result = defaultSVGViewRect;
189
193
 
190
- if (viewBox) {
191
- result = Rect2.of(viewBox);
194
+ if (!viewBox) {
195
+ this.onDetermineExportRect?.(defaultSVGViewRect);
192
196
  }
193
197
 
194
198
  this.onFinish?.();
195
- return result;
196
199
  }
197
200
 
198
201
  // TODO: Handling unsafe data! Tripple-check that this is secure!
package/src/Viewport.ts CHANGED
@@ -118,12 +118,20 @@ export class Viewport {
118
118
  return this.transform;
119
119
  }
120
120
 
121
+ public getResolution(): Vec2 {
122
+ return this.screenRect.size;
123
+ }
124
+
121
125
  // Returns the amount a vector on the canvas is scaled to become a vector on the screen.
122
126
  public getScaleFactor(): number {
123
127
  // Use transformVec3 to avoid translating the vector
124
128
  return this.transform.transformVec3(Vec3.unitX).magnitude();
125
129
  }
126
130
 
131
+ public getSizeOfPixelOnCanvas(): number {
132
+ return 1/this.getScaleFactor();
133
+ }
134
+
127
135
  // Returns the angle of the canvas in radians
128
136
  public getRotationAngle(): number {
129
137
  return this.transform.transformVec3(Vec3.unitX).angle();
@@ -158,6 +166,54 @@ export class Viewport {
158
166
  public roundPoint(point: Point2): Point2 {
159
167
  return Viewport.roundPoint(point, 1 / this.getScaleFactor());
160
168
  }
169
+
170
+ // Returns a Command that transforms the view such that [rect] is visible, and perhaps
171
+ // centered in the viewport.
172
+ // Returns null if no transformation is necessary
173
+ public zoomTo(toMakeVisible: Rect2): Command {
174
+ let transform = Mat33.identity;
175
+
176
+ // Try to move the selection within the center 2/3rds of the viewport.
177
+ const recomputeTargetRect = () => {
178
+ // transform transforms objects on the canvas. As such, we need to invert it
179
+ // to transform the viewport.
180
+ const visibleRect = this.visibleRect.transformedBoundingBox(transform.inverse());
181
+ return visibleRect.transformedBoundingBox(Mat33.scaling2D(2 / 3, visibleRect.center));
182
+ };
183
+
184
+ let targetRect = recomputeTargetRect();
185
+ const largerThanTarget = targetRect.w < toMakeVisible.w || targetRect.h < toMakeVisible.h;
186
+
187
+ // Ensure that toMakeVisible is at least 1/8th of the visible region.
188
+ const muchSmallerThanTarget = toMakeVisible.maxDimension / targetRect.maxDimension < 0.125;
189
+
190
+ if (largerThanTarget || muchSmallerThanTarget) {
191
+ // If larger than the target, ensure that the longest axis is visible.
192
+ // If smaller, shrink the visible rectangle as much as possible
193
+ const multiplier = (largerThanTarget ? Math.max : Math.min)(
194
+ toMakeVisible.w / targetRect.w, toMakeVisible.h / targetRect.h
195
+ );
196
+ const visibleRectTransform = Mat33.scaling2D(multiplier, targetRect.topLeft);
197
+ const viewportContentTransform = visibleRectTransform.inverse();
198
+
199
+ transform = transform.rightMul(viewportContentTransform);
200
+ }
201
+
202
+ targetRect = recomputeTargetRect();
203
+
204
+ // Ensure that the center of the region is visible
205
+ if (!targetRect.containsRect(toMakeVisible)) {
206
+ // target position - current position
207
+ const translation = toMakeVisible.center.minus(targetRect.center);
208
+ const visibleRectTransform = Mat33.translation(translation);
209
+ const viewportContentTransform = visibleRectTransform.inverse();
210
+
211
+ transform = transform.rightMul(viewportContentTransform);
212
+ }
213
+
214
+
215
+ return new Viewport.ViewportTransform(transform);
216
+ }
161
217
  }
162
218
 
163
219
  export namespace Viewport { // eslint-disable-line
@@ -4,13 +4,13 @@ import EditorImage from '../EditorImage';
4
4
  import LineSegment2 from '../geometry/LineSegment2';
5
5
  import Mat33 from '../geometry/Mat33';
6
6
  import Rect2 from '../geometry/Rect2';
7
- import AbstractRenderer from '../rendering/AbstractRenderer';
7
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
8
8
  import { ImageComponentLocalization } from './localization';
9
9
 
10
10
  export default abstract class AbstractComponent {
11
11
  protected lastChangedTime: number;
12
12
  protected abstract contentBBox: Rect2;
13
- public zIndex: number;
13
+ private zIndex: number;
14
14
 
15
15
  // Topmost z-index
16
16
  private static zIndexCounter: number = 0;
@@ -20,6 +20,10 @@ export default abstract class AbstractComponent {
20
20
  this.zIndex = AbstractComponent.zIndexCounter++;
21
21
  }
22
22
 
23
+ public getZIndex(): number {
24
+ return this.zIndex;
25
+ }
26
+
23
27
  public getBBox(): Rect2 {
24
28
  return this.contentBBox;
25
29
  }
@@ -1,8 +1,8 @@
1
1
  import LineSegment2 from '../geometry/LineSegment2';
2
2
  import Mat33 from '../geometry/Mat33';
3
3
  import Rect2 from '../geometry/Rect2';
4
- import AbstractRenderer from '../rendering/AbstractRenderer';
5
- import SVGRenderer from '../rendering/SVGRenderer';
4
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
+ import SVGRenderer from '../rendering/renderers/SVGRenderer';
6
6
  import AbstractComponent from './AbstractComponent';
7
7
  import { ImageComponentLocalization } from './localization';
8
8
 
@@ -2,7 +2,7 @@ import LineSegment2 from '../geometry/LineSegment2';
2
2
  import Mat33 from '../geometry/Mat33';
3
3
  import Path from '../geometry/Path';
4
4
  import Rect2 from '../geometry/Rect2';
5
- import AbstractRenderer, { RenderablePathSpec, RenderingStyle } from '../rendering/AbstractRenderer';
5
+ import AbstractRenderer, { RenderablePathSpec, RenderingStyle } from '../rendering/renderers/AbstractRenderer';
6
6
  import AbstractComponent from './AbstractComponent';
7
7
  import { ImageComponentLocalization } from './localization';
8
8
 
@@ -1,8 +1,8 @@
1
1
  import LineSegment2 from '../geometry/LineSegment2';
2
2
  import Mat33 from '../geometry/Mat33';
3
3
  import Rect2 from '../geometry/Rect2';
4
- import AbstractRenderer from '../rendering/AbstractRenderer';
5
- import SVGRenderer from '../rendering/SVGRenderer';
4
+ import AbstractRenderer from '../rendering/renderers/AbstractRenderer';
5
+ import SVGRenderer from '../rendering/renderers/SVGRenderer';
6
6
  import AbstractComponent from './AbstractComponent';
7
7
  import { ImageComponentLocalization } from './localization';
8
8
 
@@ -1,6 +1,6 @@
1
1
  import { PathCommandType } from '../../geometry/Path';
2
2
  import Rect2 from '../../geometry/Rect2';
3
- import AbstractRenderer from '../../rendering/AbstractRenderer';
3
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
4
4
  import { StrokeDataPoint } from '../../types';
5
5
  import Viewport from '../../Viewport';
6
6
  import AbstractComponent from '../AbstractComponent';
@@ -1,5 +1,5 @@
1
1
  import { Bezier } from 'bezier-js';
2
- import AbstractRenderer, { RenderingStyle, RenderablePathSpec } from '../../rendering/AbstractRenderer';
2
+ import AbstractRenderer, { RenderingStyle, RenderablePathSpec } from '../../rendering/renderers/AbstractRenderer';
3
3
  import { Point2, Vec2 } from '../../geometry/Vec2';
4
4
  import Rect2 from '../../geometry/Rect2';
5
5
  import { PathCommand, PathCommandType } from '../../geometry/Path';
@@ -1,6 +1,6 @@
1
1
  import { PathCommandType } from '../../geometry/Path';
2
2
  import Rect2 from '../../geometry/Rect2';
3
- import AbstractRenderer from '../../rendering/AbstractRenderer';
3
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
4
4
  import { StrokeDataPoint } from '../../types';
5
5
  import Viewport from '../../Viewport';
6
6
  import AbstractComponent from '../AbstractComponent';
@@ -1,6 +1,6 @@
1
1
  import Path from '../../geometry/Path';
2
2
  import Rect2 from '../../geometry/Rect2';
3
- import AbstractRenderer from '../../rendering/AbstractRenderer';
3
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
4
4
  import { StrokeDataPoint } from '../../types';
5
5
  import Viewport from '../../Viewport';
6
6
  import AbstractComponent from '../AbstractComponent';
@@ -1,5 +1,5 @@
1
1
  import Rect2 from '../../geometry/Rect2';
2
- import AbstractRenderer from '../../rendering/AbstractRenderer';
2
+ import AbstractRenderer from '../../rendering/renderers/AbstractRenderer';
3
3
  import { StrokeDataPoint } from '../../types';
4
4
  import Viewport from '../../Viewport';
5
5
  import AbstractComponent from '../AbstractComponent';
@@ -7,6 +7,9 @@ import Vec3 from './Vec3';
7
7
  export default class Mat33 {
8
8
  private readonly rows: Vec3[];
9
9
 
10
+ // ⎡ a1 a2 a3 ⎤
11
+ // ⎢ b1 b2 b3 ⎥
12
+ // ⎣ c1 c2 c3 ⎦
10
13
  public constructor(
11
14
  public readonly a1: number,
12
15
  public readonly a2: number,
@@ -19,13 +19,23 @@ describe('Path.toString', () => {
19
19
  });
20
20
 
21
21
  it('should fix rounding errors', () => {
22
- const path = new Path(Vec2.of(0.100001, 0.199999), [
22
+ const path = new Path(Vec2.of(0.10000001, 0.19999999), [
23
23
  {
24
24
  kind: PathCommandType.QuadraticBezierTo,
25
25
  controlPoint: Vec2.of(9999, -10.999999995),
26
- endPoint: Vec2.of(0.000300001, 1.400002),
26
+ endPoint: Vec2.of(0.000300001, 1.40000002),
27
27
  },
28
28
  ]);
29
29
  expect(path.toString()).toBe('M0.1,0.2Q9999,-11 0.0003,1.4');
30
30
  });
31
+
32
+ it('should not remove trailing zeroes before decimal points', () => {
33
+ const path = new Path(Vec2.of(1000, 2_000_000), [
34
+ {
35
+ kind: PathCommandType.LineTo,
36
+ point: Vec2.of(30.0001, 40.000000001),
37
+ },
38
+ ]);
39
+ expect(path.toString()).toBe('M1000,2000000L30.0001,40');
40
+ });
31
41
  });
@@ -1,5 +1,5 @@
1
1
  import { Bezier } from 'bezier-js';
2
- import { RenderingStyle, RenderablePathSpec } from '../rendering/AbstractRenderer';
2
+ import { RenderingStyle, RenderablePathSpec } from '../rendering/renderers/AbstractRenderer';
3
3
  import LineSegment2 from './LineSegment2';
4
4
  import Mat33 from './Mat33';
5
5
  import Rect2 from './Rect2';
@@ -288,8 +288,8 @@ export default class Path {
288
288
  const toRoundedString = (num: number): string => {
289
289
  // Try to remove rounding errors. If the number ends in at least three/four zeroes
290
290
  // (or nines) just one or two digits, it's probably a rounding error.
291
- const fixRoundingUpExp = /^([-]?\d*\.?\d*[1-9.])0{4,}\d$/;
292
- const hasRoundingDownExp = /^([-]?)(\d*)\.(\d*9{4,}\d)$/;
291
+ const fixRoundingUpExp = /^([-]?\d*\.\d{3,})0{4,}\d$/;
292
+ const hasRoundingDownExp = /^([-]?)(\d*)\.(\d{3,}9{4,}\d)$/;
293
293
 
294
294
  let text = num.toString();
295
295
  if (text.indexOf('.') === -1) {
@@ -314,7 +314,11 @@ export default class Path {
314
314
  }
315
315
 
316
316
  text = text.replace(fixRoundingUpExp, '$1');
317
- // Remove trailing period (if it exists)
317
+
318
+ // Remove trailing zeroes
319
+ text = text.replace(/([.][^0]*)0+$/, '$1');
320
+
321
+ // Remove trailing period
318
322
  return text.replace(/[.]$/, '');
319
323
  };
320
324
 
@@ -6,8 +6,8 @@ import Mat33 from './Mat33';
6
6
 
7
7
  loadExpectExtensions();
8
8
 
9
- describe('Rect2 tests', () => {
10
- it('Positive width, height', () => {
9
+ describe('Rect2', () => {
10
+ it('width, height should always be positive', () => {
11
11
  expect(new Rect2(-1, -2, -3, 4)).objEq(new Rect2(-4, -2, 3, 4));
12
12
  expect(new Rect2(0, 0, 0, 0).size).objEq(Vec2.zero);
13
13
  expect(Rect2.fromCorners(
@@ -19,7 +19,7 @@ describe('Rect2 tests', () => {
19
19
  ));
20
20
  });
21
21
 
22
- it('Bounding box', () => {
22
+ it('bounding boxes should be correctly computed', () => {
23
23
  expect(Rect2.bboxOf([
24
24
  Vec2.zero,
25
25
  ])).objEq(Rect2.empty);
@@ -42,14 +42,14 @@ describe('Rect2 tests', () => {
42
42
  ));
43
43
  });
44
44
 
45
- it('"union"ing', () => {
45
+ it('"union"s should contain both composite rectangles.', () => {
46
46
  expect(new Rect2(0, 0, 1, 1).union(new Rect2(1, 1, 2, 2))).objEq(
47
47
  new Rect2(0, 0, 3, 3)
48
48
  );
49
49
  expect(Rect2.empty.union(Rect2.empty)).objEq(Rect2.empty);
50
50
  });
51
51
 
52
- it('contains', () => {
52
+ it('should contain points that are within a rectangle', () => {
53
53
  expect(new Rect2(-1, -1, 2, 2).containsPoint(Vec2.zero)).toBe(true);
54
54
  expect(new Rect2(-1, -1, 0, 0).containsPoint(Vec2.zero)).toBe(false);
55
55
  expect(new Rect2(1, 2, 3, 4).containsRect(Rect2.empty)).toBe(false);
@@ -67,14 +67,15 @@ describe('Rect2 tests', () => {
67
67
  expect(Rect2.empty.containsRect(new Rect2(-1, -1, 3, 3))).toBe(false);
68
68
  });
69
69
 
70
- it('Intersection testing', () => {
70
+ it('intersecting rectangles should be identified as intersecting', () => {
71
71
  expect(new Rect2(-1, -1, 2, 2).intersects(Rect2.empty)).toBe(true);
72
72
  expect(new Rect2(-1, -1, 2, 2).intersects(new Rect2(0, 0, 1, 1))).toBe(true);
73
73
  expect(new Rect2(-1, -1, 2, 2).intersects(new Rect2(0, 0, 10, 10))).toBe(true);
74
74
  expect(new Rect2(-1, -1, 2, 2).intersects(new Rect2(3, 3, 10, 10))).toBe(false);
75
+ expect(new Rect2(-1, -1, 2, 2).intersects(new Rect2(0.2, 0.1, 0, 0))).toBe(true);
75
76
  });
76
77
 
77
- it('Computing intersections', () => {
78
+ it('intersecting rectangles should have their intersections correctly computed', () => {
78
79
  expect(new Rect2(-1, -1, 2, 2).intersection(Rect2.empty)).objEq(Rect2.empty);
79
80
  expect(new Rect2(-1, -1, 2, 2).intersection(new Rect2(0, 0, 3, 3))).objEq(
80
81
  new Rect2(0, 0, 1, 1)
@@ -93,7 +94,7 @@ describe('Rect2 tests', () => {
93
94
  expect(transformedBBox.containsRect(rect)).toBe(true);
94
95
  });
95
96
 
96
- describe('Grown to include a point', () => {
97
+ describe('should correctly expand to include a given point', () => {
97
98
  it('Growing an empty rectange to include (1, 0)', () => {
98
99
  const originalRect = Rect2.empty;
99
100
  const grownRect = originalRect.grownToPoint(Vec2.unitX);
@@ -118,4 +119,33 @@ describe('Rect2 tests', () => {
118
119
  expect(grown).objEq(new Rect2(0, 0, 2, 2));
119
120
  });
120
121
  });
122
+
123
+ describe('divideIntoGrid', () => {
124
+ it('division of unit square', () => {
125
+ expect(Rect2.unitSquare.divideIntoGrid(2, 2)).toMatchObject(
126
+ [
127
+ new Rect2(0, 0, 0.5, 0.5), new Rect2(0.5, 0, 0.5, 0.5),
128
+ new Rect2(0, 0.5, 0.5, 0.5), new Rect2(0.5, 0.5, 0.5, 0.5),
129
+ ]
130
+ );
131
+ expect(Rect2.unitSquare.divideIntoGrid(0, 0).length).toBe(0);
132
+ expect(Rect2.unitSquare.divideIntoGrid(100, 0).length).toBe(0);
133
+ expect(Rect2.unitSquare.divideIntoGrid(4, 1)).toMatchObject(
134
+ [
135
+ new Rect2(0, 0, 0.25, 1), new Rect2(0.25, 0, 0.25, 1),
136
+ new Rect2(0.5, 0, 0.25, 1), new Rect2(0.75, 0, 0.25, 1),
137
+ ]
138
+ );
139
+ });
140
+ it('division of translated square', () => {
141
+ expect(new Rect2(3, -3, 4, 4).divideIntoGrid(2, 1)).toMatchObject(
142
+ [
143
+ new Rect2(3, -3, 2, 4), new Rect2(5, -3, 2, 4),
144
+ ]
145
+ );
146
+ });
147
+ it('division of empty square', () => {
148
+ expect(Rect2.empty.divideIntoGrid(1000, 10000).length).toBe(1);
149
+ });
150
+ });
121
151
  });
@@ -71,7 +71,7 @@ export default class Rect2 {
71
71
  }
72
72
 
73
73
  // Returns the overlap of this and [other], or null, if no such
74
- // / overlap exists
74
+ // overlap exists
75
75
  public intersection(other: Rect2): Rect2|null {
76
76
  const topLeft = this.topLeft.zip(other.topLeft, Math.max);
77
77
  const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
@@ -97,6 +97,37 @@ export default class Rect2 {
97
97
  );
98
98
  }
99
99
 
100
+ // Returns a the subdivision of this into [columns] columns
101
+ // and [rows] rows. For example,
102
+ // Rect2.unitSquare.divideIntoGrid(2, 2)
103
+ // -> [ Rect2(0, 0, 0.5, 0.5), Rect2(0.5, 0, 0.5, 0.5), Rect2(0, 0.5, 0.5, 0.5), Rect2(0.5, 0.5, 0.5, 0.5) ]
104
+ // The rectangles are ordered in row-major order.
105
+ public divideIntoGrid(columns: number, rows: number): Rect2[] {
106
+ const result: Rect2[] = [];
107
+ if (columns <= 0 || rows <= 0) {
108
+ return result;
109
+ }
110
+
111
+ const eachRectWidth = this.w / columns;
112
+ const eachRectHeight = this.h / rows;
113
+
114
+ if (eachRectWidth === 0) {
115
+ columns = 1;
116
+ }
117
+ if (eachRectHeight === 0) {
118
+ rows = 1;
119
+ }
120
+
121
+ for (let j = 0; j < rows; j++) {
122
+ for (let i = 0; i < columns; i++) {
123
+ const x = eachRectWidth * i + this.x;
124
+ const y = eachRectHeight * j + this.y;
125
+ result.push(new Rect2(x, y, eachRectWidth, eachRectHeight));
126
+ }
127
+ }
128
+ return result;
129
+ }
130
+
100
131
  // Returns a rectangle containing this and [point].
101
132
  // [margin] is the minimum distance between the new point and the edge
102
133
  // of the resultant rectangle.