js-draw 0.1.0 → 0.1.1

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.
@@ -40,7 +40,7 @@ export declare class ImageNode {
40
40
  onContentChange(): void;
41
41
  getContent(): AbstractComponent | null;
42
42
  getParent(): ImageNode | null;
43
- getChildrenInRegion(region: Rect2): ImageNode[];
43
+ private getChildrenIntersectingRegion;
44
44
  getChildrenOrSelfIntersectingRegion(region: Rect2): ImageNode[];
45
45
  getLeavesIntersectingRegion(region: Rect2, isTooSmall?: TooSmallToRenderCheck): ImageNode[];
46
46
  getLeaves(): ImageNode[];
@@ -107,7 +107,7 @@ export class ImageNode {
107
107
  getParent() {
108
108
  return this.parent;
109
109
  }
110
- getChildrenInRegion(region) {
110
+ getChildrenIntersectingRegion(region) {
111
111
  return this.children.filter(child => {
112
112
  return child.getBBox().intersects(region);
113
113
  });
@@ -116,7 +116,7 @@ export class ImageNode {
116
116
  if (this.content) {
117
117
  return [this];
118
118
  }
119
- return this.getChildrenInRegion(region);
119
+ return this.getChildrenIntersectingRegion(region);
120
120
  }
121
121
  // Returns a list of `ImageNode`s with content (and thus no children).
122
122
  getLeavesIntersectingRegion(region, isTooSmall) {
@@ -128,7 +128,7 @@ export class ImageNode {
128
128
  if (this.content !== null && this.getBBox().intersects(region)) {
129
129
  result.push(this);
130
130
  }
131
- const children = this.getChildrenInRegion(region);
131
+ const children = this.getChildrenIntersectingRegion(region);
132
132
  for (const child of children) {
133
133
  result.push(...child.getLeavesIntersectingRegion(region, isTooSmall));
134
134
  }
@@ -160,7 +160,7 @@ export default class FreehandLineBuilder {
160
160
  const upperBoundary = computeBoundaryCurve(1, halfVec);
161
161
  const lowerBoundary = computeBoundaryCurve(-1, halfVec);
162
162
  // If the boundaries have two intersections, increasing the half vector's length could fix this.
163
- if (upperBoundary.intersects(lowerBoundary).length === 2) {
163
+ if (upperBoundary.intersects(lowerBoundary).length > 0) {
164
164
  halfVec = halfVec.times(2);
165
165
  }
166
166
  const pathCommands = [
@@ -38,20 +38,31 @@ export default class Rect2 {
38
38
  && this.bottomRight.y >= other.bottomRight.y;
39
39
  }
40
40
  intersects(other) {
41
- return this.intersection(other) !== null;
41
+ // Project along x/y axes.
42
+ const thisMinX = this.x;
43
+ const thisMaxX = thisMinX + this.w;
44
+ const otherMinX = other.x;
45
+ const otherMaxX = other.x + other.w;
46
+ if (thisMaxX < otherMinX || thisMinX > otherMaxX) {
47
+ return false;
48
+ }
49
+ const thisMinY = this.y;
50
+ const thisMaxY = thisMinY + this.h;
51
+ const otherMinY = other.y;
52
+ const otherMaxY = other.y + other.h;
53
+ if (thisMaxY < otherMinY || thisMinY > otherMaxY) {
54
+ return false;
55
+ }
56
+ return true;
42
57
  }
43
58
  // Returns the overlap of this and [other], or null, if no such
44
59
  // overlap exists
45
60
  intersection(other) {
46
- const topLeft = this.topLeft.zip(other.topLeft, Math.max);
47
- const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
48
- // The intersection can't be outside of this rectangle
49
- if (!this.containsPoint(topLeft) || !this.containsPoint(bottomRight)) {
50
- return null;
51
- }
52
- else if (!other.containsPoint(topLeft) || !other.containsPoint(bottomRight)) {
61
+ if (!this.intersects(other)) {
53
62
  return null;
54
63
  }
64
+ const topLeft = this.topLeft.zip(other.topLeft, Math.max);
65
+ const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
55
66
  return Rect2.fromCorners(topLeft, bottomRight);
56
67
  }
57
68
  // Returns a new rectangle containing both [this] and [other].
@@ -23,7 +23,7 @@ export default class Display {
23
23
  else {
24
24
  throw new Error(`Unknown rendering mode, ${mode}!`);
25
25
  }
26
- const cacheBlockResolution = Vec2.of(500, 500);
26
+ const cacheBlockResolution = Vec2.of(600, 600);
27
27
  this.cache = new RenderingCache({
28
28
  createRenderer: () => {
29
29
  if (mode === RenderingMode.DummyRenderer) {
@@ -45,8 +45,9 @@ export default class Display {
45
45
  },
46
46
  blockResolution: cacheBlockResolution,
47
47
  cacheSize: 500 * 500 * 4 * 200,
48
- maxScale: 1.4,
49
- minComponentsPerCache: 10,
48
+ maxScale: 1.5,
49
+ minComponentsPerCache: 50,
50
+ minComponentsToUseCache: 120,
50
51
  });
51
52
  this.editor.notifier.on(EditorEventType.DisplayResized, event => {
52
53
  var _a;
@@ -11,7 +11,7 @@ export default class CacheRecord {
11
11
  this.allocd = true;
12
12
  }
13
13
  startRender() {
14
- this.lastUsedCycle = this.cacheState.currentRenderingCycle++;
14
+ this.lastUsedCycle = this.cacheState.currentRenderingCycle;
15
15
  if (!this.allocd) {
16
16
  throw new Error('Only alloc\'d canvases can be rendered to');
17
17
  }
@@ -33,6 +33,7 @@ export default class CacheRecord {
33
33
  }
34
34
  this.allocd = true;
35
35
  this.onBeforeDeallocCallback = newDeallocCallback;
36
+ this.lastUsedCycle = this.cacheState.currentRenderingCycle;
36
37
  }
37
38
  getLastUsedCycle() {
38
39
  return this.lastUsedCycle;
@@ -25,15 +25,7 @@ export class CacheRecordManager {
25
25
  }
26
26
  // Returns null if there are no cache records. Returns an unalloc'd record if one exists.
27
27
  getLeastRecentlyUsedRecord() {
28
- let lruSoFar = null;
29
- for (const rec of this.cacheRecords) {
30
- if (!rec.isAllocd()) {
31
- return rec;
32
- }
33
- if (!lruSoFar || rec.getLastUsedCycle() < lruSoFar.getLastUsedCycle()) {
34
- lruSoFar = rec;
35
- }
36
- }
37
- return lruSoFar;
28
+ this.cacheRecords.sort((a, b) => a.getLastUsedCycle() - b.getLastUsedCycle());
29
+ return this.cacheRecords[0];
38
30
  }
39
31
  }
@@ -22,15 +22,21 @@ export default class RenderingCache {
22
22
  return;
23
23
  }
24
24
  if (!this.rootNode) {
25
- // Ensure that the node is just big enough to contain the entire viewport.
26
- const rootNodeSize = visibleRect.maxDimension;
25
+ // Adjust the node so that it has the correct aspect ratio
26
+ const res = this.partialSharedState.props.blockResolution;
27
27
  const topLeft = visibleRect.topLeft;
28
- this.rootNode = new RenderingCacheNode(new Rect2(topLeft.x, topLeft.y, rootNodeSize, rootNodeSize), this.getSharedState());
28
+ this.rootNode = new RenderingCacheNode(new Rect2(topLeft.x, topLeft.y, res.x, res.y), this.getSharedState());
29
29
  }
30
30
  while (!this.rootNode.region.containsRect(visibleRect)) {
31
31
  this.rootNode = this.rootNode.generateParent();
32
32
  }
33
33
  this.rootNode = (_a = this.rootNode.smallestChildContaining(visibleRect)) !== null && _a !== void 0 ? _a : this.rootNode;
34
- this.rootNode.renderItems(screenRenderer, [image], viewport);
34
+ const visibleLeaves = image.getLeavesIntersectingRegion(viewport.visibleRect, rect => screenRenderer.isTooSmallToRender(rect));
35
+ if (visibleLeaves.length > this.partialSharedState.props.minComponentsToUseCache) {
36
+ this.rootNode.renderItems(screenRenderer, [image], viewport);
37
+ }
38
+ else {
39
+ image.render(screenRenderer, visibleRect);
40
+ }
35
41
  }
36
42
  }
@@ -133,7 +133,11 @@ export default class RenderingCacheNode {
133
133
  const newItems = [];
134
134
  // Divide [items] until nodes are leaves or smaller than this
135
135
  for (const item of items) {
136
- if (item.getBBox().maxDimension >= this.region.maxDimension) {
136
+ const bbox = item.getBBox();
137
+ if (!bbox.intersects(this.region)) {
138
+ continue;
139
+ }
140
+ if (bbox.maxDimension >= this.region.maxDimension) {
137
141
  newItems.push(...item.getChildrenOrSelfIntersectingRegion(this.region));
138
142
  }
139
143
  else {
@@ -146,6 +150,9 @@ export default class RenderingCacheNode {
146
150
  items.forEach(item => item.render(screenRenderer, viewport.visibleRect));
147
151
  return;
148
152
  }
153
+ if (debugMode) {
154
+ screenRenderer.drawRect(this.region, 0.5 * viewport.getSizeOfPixelOnCanvas(), { fill: Color4.yellow });
155
+ }
149
156
  // Could we render direclty from [this] or do we need to recurse?
150
157
  const couldRender = this.renderingWouldBeHighEnoughResolution(viewport);
151
158
  if (!couldRender) {
@@ -159,7 +166,7 @@ export default class RenderingCacheNode {
159
166
  // Determine whether we already have rendered the items
160
167
  const leaves = [];
161
168
  for (const item of items) {
162
- leaves.push(...item.getLeavesIntersectingRegion(this.region));
169
+ leaves.push(...item.getLeavesIntersectingRegion(this.region, rect => rect.w / this.region.w < 2 / this.cacheState.props.blockResolution.x));
163
170
  }
164
171
  sortLeavesByZIndex(leaves);
165
172
  const leavesByIds = this.computeSortedByLeafIds(leaves);
@@ -213,7 +220,7 @@ export default class RenderingCacheNode {
213
220
  }
214
221
  }
215
222
  if (debugMode) {
216
- screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: Color4.yellow });
223
+ screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: Color4.clay });
217
224
  }
218
225
  }
219
226
  }
@@ -12,7 +12,7 @@ export const createCache = (onRenderAlloc, cacheOptions) => {
12
12
  },
13
13
  isOfCorrectType(renderer) {
14
14
  return renderer instanceof DummyRenderer;
15
- }, blockResolution: Vec2.of(500, 500), cacheSize: 500 * 10 * 4, maxScale: 2, minComponentsPerCache: 0 }, cacheOptions));
15
+ }, blockResolution: Vec2.of(500, 500), cacheSize: 500 * 10 * 4, maxScale: 2, minComponentsPerCache: 0, minComponentsToUseCache: 0 }, cacheOptions));
16
16
  return {
17
17
  cache,
18
18
  editor
@@ -10,6 +10,7 @@ export interface CacheProps {
10
10
  cacheSize: number;
11
11
  maxScale: number;
12
12
  minComponentsPerCache: number;
13
+ minComponentsToUseCache: number;
13
14
  }
14
15
  export interface PartialCacheState {
15
16
  currentRenderingCycle: number;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "js-draw",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Draw pictures using a pen, touchscreen, or mouse! JS-draw is a drawing library for JavaScript and TypeScript. ",
5
5
  "main": "dist/src/Editor.js",
6
6
  "types": "dist/src/Editor.d.ts",
@@ -137,7 +137,7 @@ export class ImageNode {
137
137
  return this.parent;
138
138
  }
139
139
 
140
- public getChildrenInRegion(region: Rect2): ImageNode[] {
140
+ private getChildrenIntersectingRegion(region: Rect2): ImageNode[] {
141
141
  return this.children.filter(child => {
142
142
  return child.getBBox().intersects(region);
143
143
  });
@@ -147,7 +147,7 @@ export class ImageNode {
147
147
  if (this.content) {
148
148
  return [this];
149
149
  }
150
- return this.getChildrenInRegion(region);
150
+ return this.getChildrenIntersectingRegion(region);
151
151
  }
152
152
 
153
153
  // Returns a list of `ImageNode`s with content (and thus no children).
@@ -163,7 +163,7 @@ export class ImageNode {
163
163
  result.push(this);
164
164
  }
165
165
 
166
- const children = this.getChildrenInRegion(region);
166
+ const children = this.getChildrenIntersectingRegion(region);
167
167
  for (const child of children) {
168
168
  result.push(...child.getLeavesIntersectingRegion(region, isTooSmall));
169
169
  }
@@ -212,7 +212,7 @@ export default class FreehandLineBuilder implements ComponentBuilder {
212
212
  const lowerBoundary = computeBoundaryCurve(-1, halfVec);
213
213
 
214
214
  // If the boundaries have two intersections, increasing the half vector's length could fix this.
215
- if (upperBoundary.intersects(lowerBoundary).length === 2) {
215
+ if (upperBoundary.intersects(lowerBoundary).length > 0) {
216
216
  halfVec = halfVec.times(2);
217
217
  }
218
218
 
@@ -148,4 +148,13 @@ describe('Rect2', () => {
148
148
  expect(Rect2.empty.divideIntoGrid(1000, 10000).length).toBe(1);
149
149
  });
150
150
  });
151
+
152
+ it('division of rectangle', () => {
153
+ expect(new Rect2(0, 0, 2, 1).divideIntoGrid(2, 2)).toMatchObject(
154
+ [
155
+ new Rect2(0, 0, 1, 0.5), new Rect2(1, 0, 1, 0.5),
156
+ new Rect2(0, 0.5, 1, 0.5), new Rect2(1, 0.5, 1, 0.5),
157
+ ]
158
+ );
159
+ });
151
160
  });
@@ -67,22 +67,39 @@ export default class Rect2 {
67
67
  }
68
68
 
69
69
  public intersects(other: Rect2): boolean {
70
- return this.intersection(other) !== null;
70
+ // Project along x/y axes.
71
+ const thisMinX = this.x;
72
+ const thisMaxX = thisMinX + this.w;
73
+ const otherMinX = other.x;
74
+ const otherMaxX = other.x + other.w;
75
+
76
+ if (thisMaxX < otherMinX || thisMinX > otherMaxX) {
77
+ return false;
78
+ }
79
+
80
+
81
+ const thisMinY = this.y;
82
+ const thisMaxY = thisMinY + this.h;
83
+ const otherMinY = other.y;
84
+ const otherMaxY = other.y + other.h;
85
+
86
+ if (thisMaxY < otherMinY || thisMinY > otherMaxY) {
87
+ return false;
88
+ }
89
+
90
+ return true;
71
91
  }
72
92
 
73
93
  // Returns the overlap of this and [other], or null, if no such
74
94
  // overlap exists
75
95
  public intersection(other: Rect2): Rect2|null {
76
- const topLeft = this.topLeft.zip(other.topLeft, Math.max);
77
- const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
78
-
79
- // The intersection can't be outside of this rectangle
80
- if (!this.containsPoint(topLeft) || !this.containsPoint(bottomRight)) {
81
- return null;
82
- } else if (!other.containsPoint(topLeft) || !other.containsPoint(bottomRight)) {
96
+ if (!this.intersects(other)) {
83
97
  return null;
84
98
  }
85
99
 
100
+ const topLeft = this.topLeft.zip(other.topLeft, Math.max);
101
+ const bottomRight = this.bottomRight.zip(other.bottomRight, Math.min);
102
+
86
103
  return Rect2.fromCorners(topLeft, bottomRight);
87
104
  }
88
105
 
@@ -31,7 +31,7 @@ export default class Display {
31
31
  throw new Error(`Unknown rendering mode, ${mode}!`);
32
32
  }
33
33
 
34
- const cacheBlockResolution = Vec2.of(500, 500);
34
+ const cacheBlockResolution = Vec2.of(600, 600);
35
35
  this.cache = new RenderingCache({
36
36
  createRenderer: () => {
37
37
  if (mode === RenderingMode.DummyRenderer) {
@@ -54,8 +54,9 @@ export default class Display {
54
54
  },
55
55
  blockResolution: cacheBlockResolution,
56
56
  cacheSize: 500 * 500 * 4 * 200,
57
- maxScale: 1.4,
58
- minComponentsPerCache: 10,
57
+ maxScale: 1.5,
58
+ minComponentsPerCache: 50,
59
+ minComponentsToUseCache: 120,
59
60
  });
60
61
 
61
62
  this.editor.notifier.on(EditorEventType.DisplayResized, event => {
@@ -21,7 +21,7 @@ export default class CacheRecord {
21
21
  }
22
22
 
23
23
  public startRender(): AbstractRenderer {
24
- this.lastUsedCycle = this.cacheState.currentRenderingCycle++;
24
+ this.lastUsedCycle = this.cacheState.currentRenderingCycle;
25
25
  if (!this.allocd) {
26
26
  throw new Error('Only alloc\'d canvases can be rendered to');
27
27
  }
@@ -45,6 +45,7 @@ export default class CacheRecord {
45
45
  }
46
46
  this.allocd = true;
47
47
  this.onBeforeDeallocCallback = newDeallocCallback;
48
+ this.lastUsedCycle = this.cacheState.currentRenderingCycle;
48
49
  }
49
50
 
50
51
  public getLastUsedCycle(): number {
@@ -39,17 +39,7 @@ export class CacheRecordManager {
39
39
 
40
40
  // Returns null if there are no cache records. Returns an unalloc'd record if one exists.
41
41
  private getLeastRecentlyUsedRecord(): CacheRecord|null {
42
- let lruSoFar: CacheRecord|null = null;
43
- for (const rec of this.cacheRecords) {
44
- if (!rec.isAllocd()) {
45
- return rec;
46
- }
47
-
48
- if (!lruSoFar || rec.getLastUsedCycle() < lruSoFar.getLastUsedCycle()) {
49
- lruSoFar = rec;
50
- }
51
- }
52
-
53
- return lruSoFar;
42
+ this.cacheRecords.sort((a, b) => a.getLastUsedCycle() - b.getLastUsedCycle());
43
+ return this.cacheRecords[0];
54
44
  }
55
45
  }
@@ -10,7 +10,7 @@ import Viewport from '../../Viewport';
10
10
  import Mat33 from '../../geometry/Mat33';
11
11
 
12
12
  describe('RenderingCache', () => {
13
- const testPath = Path.fromString('M0,0 l100,500 l-20,20');
13
+ const testPath = Path.fromString('M0,0 l100,500 l-20,20 L-100,-100');
14
14
  const testStroke = new Stroke([ testPath.toRenderable({ fill: Color4.purple }) ]);
15
15
 
16
16
  it('should create a root node large enough to contain the viewport', () => {
@@ -37,11 +37,12 @@ export default class RenderingCache {
37
37
  }
38
38
 
39
39
  if (!this.rootNode) {
40
- // Ensure that the node is just big enough to contain the entire viewport.
41
- const rootNodeSize = visibleRect.maxDimension;
40
+ // Adjust the node so that it has the correct aspect ratio
41
+ const res = this.partialSharedState.props.blockResolution;
42
+
42
43
  const topLeft = visibleRect.topLeft;
43
44
  this.rootNode = new RenderingCacheNode(
44
- new Rect2(topLeft.x, topLeft.y, rootNodeSize, rootNodeSize),
45
+ new Rect2(topLeft.x, topLeft.y, res.x, res.y),
45
46
  this.getSharedState()
46
47
  );
47
48
  }
@@ -51,6 +52,12 @@ export default class RenderingCache {
51
52
  }
52
53
 
53
54
  this.rootNode = this.rootNode!.smallestChildContaining(visibleRect) ?? this.rootNode;
54
- this.rootNode!.renderItems(screenRenderer, [ image ], viewport);
55
+
56
+ const visibleLeaves = image.getLeavesIntersectingRegion(viewport.visibleRect, rect => screenRenderer.isTooSmallToRender(rect));
57
+ if (visibleLeaves.length > this.partialSharedState.props.minComponentsToUseCache) {
58
+ this.rootNode!.renderItems(screenRenderer, [ image ], viewport);
59
+ } else {
60
+ image.render(screenRenderer, visibleRect);
61
+ }
55
62
  }
56
63
  }
@@ -172,7 +172,12 @@ export default class RenderingCacheNode {
172
172
  const newItems = [];
173
173
  // Divide [items] until nodes are leaves or smaller than this
174
174
  for (const item of items) {
175
- if (item.getBBox().maxDimension >= this.region.maxDimension) {
175
+ const bbox = item.getBBox();
176
+ if (!bbox.intersects(this.region)) {
177
+ continue;
178
+ }
179
+
180
+ if (bbox.maxDimension >= this.region.maxDimension) {
176
181
  newItems.push(...item.getChildrenOrSelfIntersectingRegion(this.region));
177
182
  } else {
178
183
  newItems.push(item);
@@ -186,6 +191,10 @@ export default class RenderingCacheNode {
186
191
  return;
187
192
  }
188
193
 
194
+ if (debugMode) {
195
+ screenRenderer.drawRect(this.region, 0.5 * viewport.getSizeOfPixelOnCanvas(), { fill: Color4.yellow });
196
+ }
197
+
189
198
  // Could we render direclty from [this] or do we need to recurse?
190
199
  const couldRender = this.renderingWouldBeHighEnoughResolution(viewport);
191
200
  if (!couldRender) {
@@ -198,7 +207,11 @@ export default class RenderingCacheNode {
198
207
  // Determine whether we already have rendered the items
199
208
  const leaves = [];
200
209
  for (const item of items) {
201
- leaves.push(...item.getLeavesIntersectingRegion(this.region));
210
+ leaves.push(
211
+ ...item.getLeavesIntersectingRegion(
212
+ this.region, rect => rect.w / this.region.w < 2 / this.cacheState.props.blockResolution.x,
213
+ )
214
+ );
202
215
  }
203
216
  sortLeavesByZIndex(leaves);
204
217
  const leavesByIds = this.computeSortedByLeafIds(leaves);
@@ -266,7 +279,7 @@ export default class RenderingCacheNode {
266
279
  }
267
280
 
268
281
  if (debugMode) {
269
- screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: Color4.yellow });
282
+ screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: Color4.clay });
270
283
  }
271
284
  }
272
285
  }
@@ -24,6 +24,7 @@ export const createCache = (onRenderAlloc?: RenderAllocCallback, cacheOptions?:
24
24
  cacheSize: 500 * 10 * 4,
25
25
  maxScale: 2,
26
26
  minComponentsPerCache: 0,
27
+ minComponentsToUseCache: 0,
27
28
  ...cacheOptions
28
29
  });
29
30
 
@@ -21,6 +21,10 @@ export interface CacheProps {
21
21
 
22
22
  // Minimum component count to cache, rather than just re-render each time.
23
23
  minComponentsPerCache: number;
24
+
25
+ // Minimum number of strokes/etc. to use the cache to render, isntead of
26
+ // rendering directly.
27
+ minComponentsToUseCache: number;
24
28
  }
25
29
 
26
30
  // CacheRecordManager relies on a partial copy of the shared state. Thus,