js-draw 1.7.2 → 1.9.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 (68) hide show
  1. package/LICENSE +1 -1
  2. package/dist/Editor.css +1 -0
  3. package/dist/bundle.js +2 -2
  4. package/dist/bundledStyles.js +1 -1
  5. package/dist/cjs/EventDispatcher.js +2 -1
  6. package/dist/cjs/SVGLoader.d.ts +1 -1
  7. package/dist/cjs/SVGLoader.js +11 -2
  8. package/dist/cjs/Viewport.d.ts +2 -0
  9. package/dist/cjs/Viewport.js +2 -0
  10. package/dist/cjs/components/AbstractComponent.d.ts +9 -0
  11. package/dist/cjs/components/AbstractComponent.js +11 -0
  12. package/dist/cjs/components/Stroke.d.ts +3 -0
  13. package/dist/cjs/components/Stroke.js +55 -1
  14. package/dist/cjs/image/EditorImage.d.ts +20 -0
  15. package/dist/cjs/image/EditorImage.js +48 -3
  16. package/dist/cjs/rendering/Display.d.ts +9 -0
  17. package/dist/cjs/rendering/Display.js +39 -5
  18. package/dist/cjs/rendering/RenderablePathSpec.d.ts +20 -2
  19. package/dist/cjs/rendering/RenderablePathSpec.js +72 -9
  20. package/dist/cjs/rendering/caching/CacheRecord.js +5 -3
  21. package/dist/cjs/rendering/caching/CacheRecordManager.js +3 -4
  22. package/dist/cjs/rendering/caching/RenderingCache.d.ts +1 -0
  23. package/dist/cjs/rendering/caching/RenderingCache.js +4 -0
  24. package/dist/cjs/rendering/caching/RenderingCacheNode.js +19 -11
  25. package/dist/cjs/rendering/caching/types.d.ts +1 -0
  26. package/dist/cjs/rendering/renderers/AbstractRenderer.d.ts +10 -0
  27. package/dist/cjs/rendering/renderers/AbstractRenderer.js +12 -3
  28. package/dist/cjs/rendering/renderers/CanvasRenderer.js +5 -2
  29. package/dist/cjs/rendering/renderers/SVGRenderer.d.ts +0 -10
  30. package/dist/cjs/rendering/renderers/SVGRenderer.js +0 -14
  31. package/dist/cjs/testing/startPinchGesture.d.ts +14 -0
  32. package/dist/cjs/testing/startPinchGesture.js +41 -0
  33. package/dist/cjs/tools/PanZoom.d.ts +4 -0
  34. package/dist/cjs/tools/PanZoom.js +21 -2
  35. package/dist/cjs/version.js +1 -1
  36. package/dist/mjs/EventDispatcher.mjs +2 -1
  37. package/dist/mjs/SVGLoader.d.ts +1 -1
  38. package/dist/mjs/SVGLoader.mjs +11 -2
  39. package/dist/mjs/Viewport.d.ts +2 -0
  40. package/dist/mjs/Viewport.mjs +2 -0
  41. package/dist/mjs/components/AbstractComponent.d.ts +9 -0
  42. package/dist/mjs/components/AbstractComponent.mjs +11 -0
  43. package/dist/mjs/components/Stroke.d.ts +3 -0
  44. package/dist/mjs/components/Stroke.mjs +56 -2
  45. package/dist/mjs/image/EditorImage.d.ts +20 -0
  46. package/dist/mjs/image/EditorImage.mjs +46 -2
  47. package/dist/mjs/rendering/Display.d.ts +9 -0
  48. package/dist/mjs/rendering/Display.mjs +39 -5
  49. package/dist/mjs/rendering/RenderablePathSpec.d.ts +20 -2
  50. package/dist/mjs/rendering/RenderablePathSpec.mjs +71 -9
  51. package/dist/mjs/rendering/caching/CacheRecord.mjs +5 -3
  52. package/dist/mjs/rendering/caching/CacheRecordManager.mjs +3 -4
  53. package/dist/mjs/rendering/caching/RenderingCache.d.ts +1 -0
  54. package/dist/mjs/rendering/caching/RenderingCache.mjs +4 -0
  55. package/dist/mjs/rendering/caching/RenderingCacheNode.mjs +20 -12
  56. package/dist/mjs/rendering/caching/types.d.ts +1 -0
  57. package/dist/mjs/rendering/renderers/AbstractRenderer.d.ts +10 -0
  58. package/dist/mjs/rendering/renderers/AbstractRenderer.mjs +12 -3
  59. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +5 -2
  60. package/dist/mjs/rendering/renderers/SVGRenderer.d.ts +0 -10
  61. package/dist/mjs/rendering/renderers/SVGRenderer.mjs +0 -14
  62. package/dist/mjs/testing/startPinchGesture.d.ts +14 -0
  63. package/dist/mjs/testing/startPinchGesture.mjs +36 -0
  64. package/dist/mjs/tools/PanZoom.d.ts +4 -0
  65. package/dist/mjs/tools/PanZoom.mjs +21 -2
  66. package/dist/mjs/version.mjs +1 -1
  67. package/package.json +4 -4
  68. package/src/Editor.scss +2 -0
@@ -34,6 +34,7 @@ class Display {
34
34
  this.editor = editor;
35
35
  this.parent = parent;
36
36
  this.textRerenderOutput = null;
37
+ this.devicePixelRatio = window.devicePixelRatio ?? 1;
37
38
  /**
38
39
  * @returns the color at the given point on the dry ink renderer, or `null` if `screenPos`
39
40
  * is not on the display.
@@ -118,16 +119,29 @@ class Display {
118
119
  this.parent.appendChild(wetInkCanvas);
119
120
  }
120
121
  this.resizeSurfacesCallback = () => {
122
+ const expectedWidth = (canvas) => {
123
+ return Math.ceil(canvas.clientWidth * this.devicePixelRatio);
124
+ };
125
+ const expectedHeight = (canvas) => {
126
+ return Math.ceil(canvas.clientHeight * this.devicePixelRatio);
127
+ };
121
128
  const hasSizeMismatch = (canvas) => {
122
- return canvas.clientHeight !== canvas.height || canvas.clientWidth !== canvas.width;
129
+ return expectedHeight(canvas) !== canvas.height || expectedWidth(canvas) !== canvas.width;
123
130
  };
124
131
  // Ensure that the drawing surfaces sizes match the
125
132
  // canvas' sizes to prevent stretching.
126
133
  if (hasSizeMismatch(dryInkCanvas) || hasSizeMismatch(wetInkCanvas)) {
127
- dryInkCanvas.width = dryInkCanvas.clientWidth;
128
- dryInkCanvas.height = dryInkCanvas.clientHeight;
129
- wetInkCanvas.width = wetInkCanvas.clientWidth;
130
- wetInkCanvas.height = wetInkCanvas.clientHeight;
134
+ dryInkCanvas.width = expectedWidth(dryInkCanvas);
135
+ dryInkCanvas.height = expectedHeight(dryInkCanvas);
136
+ wetInkCanvas.width = expectedWidth(wetInkCanvas);
137
+ wetInkCanvas.height = expectedHeight(wetInkCanvas);
138
+ // Ensure correct drawing operations on high-resolution screens.
139
+ // See
140
+ // https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas#scaling_for_high_resolution_displays
141
+ wetInkCtx.resetTransform();
142
+ dryInkCtx.resetTransform();
143
+ dryInkCtx.scale(this.devicePixelRatio, this.devicePixelRatio);
144
+ wetInkCtx.scale(this.devicePixelRatio, this.devicePixelRatio);
131
145
  this.editor.notifier.dispatch(types_1.EditorEventType.DisplayResized, {
132
146
  kind: types_1.EditorEventType.DisplayResized,
133
147
  newSize: math_1.Vec2.of(this.width, this.height),
@@ -136,7 +150,10 @@ class Display {
136
150
  };
137
151
  this.resizeSurfacesCallback();
138
152
  this.flattenCallback = () => {
153
+ dryInkCtx.save();
154
+ dryInkCtx.resetTransform();
139
155
  dryInkCtx.drawImage(wetInkCanvas, 0, 0);
156
+ dryInkCtx.restore();
140
157
  };
141
158
  this.getColorAt = (screenPos) => {
142
159
  const pixel = dryInkCtx.getImageData(screenPos.x, screenPos.y, 1, 1);
@@ -162,6 +179,23 @@ class Display {
162
179
  textRendererOutputContainer.replaceChildren(rerenderButton, this.textRerenderOutput);
163
180
  this.editor.createHTMLOverlay(textRendererOutputContainer);
164
181
  }
182
+ /**
183
+ * Sets the device-pixel-ratio.
184
+ *
185
+ * Intended for debugging. Users do not need to call this manually.
186
+ *
187
+ * @internal
188
+ */
189
+ setDevicePixelRatio(dpr) {
190
+ const minDpr = 0.001;
191
+ const maxDpr = 10;
192
+ if (isFinite(dpr) && dpr >= minDpr && dpr <= maxDpr && dpr !== this.devicePixelRatio) {
193
+ this.devicePixelRatio = dpr;
194
+ this.resizeSurfacesCallback?.();
195
+ return this.editor.queueRerender();
196
+ }
197
+ return undefined;
198
+ }
165
199
  /**
166
200
  * Rerenders the text-based display.
167
201
  * The text-based display is intended for screen readers and can be navigated to by pressing `tab`.
@@ -6,11 +6,29 @@ interface RenderablePathSpec {
6
6
  style: RenderingStyle;
7
7
  path?: Path;
8
8
  }
9
+ interface RenderablePathSpecWithPath extends RenderablePathSpec {
10
+ path: Path;
11
+ }
9
12
  /** Converts a renderable path (a path with a `startPoint`, `commands`, and `style`). */
10
13
  export declare const pathFromRenderable: (renderable: RenderablePathSpec) => Path;
11
- export declare const pathToRenderable: (path: Path, style: RenderingStyle) => RenderablePathSpec;
14
+ export declare const pathToRenderable: (path: Path, style: RenderingStyle) => RenderablePathSpecWithPath;
15
+ interface RectangleSimplificationResult {
16
+ rectangle: Rect2;
17
+ path: RenderablePathSpecWithPath;
18
+ fullScreen: boolean;
19
+ }
20
+ /**
21
+ * Tries to simplify the given path to a fullscreen rectangle.
22
+ * Returns `null` on failure.
23
+ *
24
+ * @internal
25
+ */
26
+ export declare const simplifyPathToFullScreenOrEmpty: (renderablePath: RenderablePathSpec, visibleRect: Rect2, options?: {
27
+ fastCheck: boolean;
28
+ expensiveCheck: boolean;
29
+ }) => RectangleSimplificationResult | null;
12
30
  /**
13
31
  * @returns a Path that, when rendered, looks roughly equivalent to the given path.
14
32
  */
15
- export declare const visualEquivalent: (renderablePath: RenderablePathSpec, visibleRect: Rect2) => RenderablePathSpec;
33
+ export declare const visualEquivalent: (renderablePath: RenderablePathSpec, visibleRect: Rect2) => RenderablePathSpecWithPath;
16
34
  export default RenderablePathSpec;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.visualEquivalent = exports.pathToRenderable = exports.pathFromRenderable = void 0;
3
+ exports.visualEquivalent = exports.simplifyPathToFullScreenOrEmpty = exports.pathToRenderable = exports.pathFromRenderable = void 0;
4
4
  const math_1 = require("@js-draw/math");
5
5
  /** Converts a renderable path (a path with a `startPoint`, `commands`, and `style`). */
6
6
  const pathFromRenderable = (renderable) => {
@@ -20,33 +20,90 @@ const pathToRenderable = (path, style) => {
20
20
  };
21
21
  exports.pathToRenderable = pathToRenderable;
22
22
  /**
23
- * @returns a Path that, when rendered, looks roughly equivalent to the given path.
23
+ * Fills the optional `path` field in `RenderablePathSpec`
24
+ * with `path` if not already filled
24
25
  */
25
- const visualEquivalent = (renderablePath, visibleRect) => {
26
+ const pathIncluded = (renderablePath, path) => {
27
+ if (renderablePath.path) {
28
+ return renderablePath;
29
+ }
30
+ return {
31
+ ...renderablePath,
32
+ path,
33
+ };
34
+ };
35
+ /**
36
+ * Tries to simplify the given path to a fullscreen rectangle.
37
+ * Returns `null` on failure.
38
+ *
39
+ * @internal
40
+ */
41
+ const simplifyPathToFullScreenOrEmpty = (renderablePath, visibleRect, options = { fastCheck: true, expensiveCheck: true }) => {
26
42
  const path = (0, exports.pathFromRenderable)(renderablePath);
27
43
  const strokeWidth = renderablePath.style.stroke?.width ?? 0;
28
44
  const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
29
45
  const styledPathBBox = path.bbox.grownBy(strokeWidth);
30
46
  // Are we close enough to the path that it fills the entire screen?
31
- if (onlyStroked
32
- && renderablePath.style.stroke
47
+ const isOnlyStrokedAndCouldFillScreen = (onlyStroked
33
48
  && strokeWidth > visibleRect.maxDimension
34
- && styledPathBBox.containsRect(visibleRect)) {
49
+ && styledPathBBox.containsRect(visibleRect));
50
+ if (options.fastCheck && isOnlyStrokedAndCouldFillScreen && renderablePath.style.stroke) {
35
51
  const strokeRadius = strokeWidth / 2;
52
+ // Are we completely within the stroke?
36
53
  // Do a fast, but with many false negatives, check.
37
54
  for (const point of path.startEndPoints()) {
38
55
  // If within the strokeRadius of any point
39
56
  if (visibleRect.isWithinRadiusOf(strokeRadius, point)) {
40
- return (0, exports.pathToRenderable)(math_1.Path.fromRect(visibleRect), { fill: renderablePath.style.stroke.color });
57
+ return {
58
+ rectangle: visibleRect,
59
+ path: (0, exports.pathToRenderable)(math_1.Path.fromRect(visibleRect), { fill: renderablePath.style.stroke.color }),
60
+ fullScreen: true,
61
+ };
41
62
  }
42
63
  }
43
64
  }
65
+ // Try filtering again, but with slightly more expensive checks
66
+ if (options.expensiveCheck &&
67
+ isOnlyStrokedAndCouldFillScreen && renderablePath.style.stroke
68
+ && strokeWidth > visibleRect.maxDimension * 3) {
69
+ const signedDist = path.signedDistance(visibleRect.center, strokeWidth / 2);
70
+ const margin = strokeWidth / 6;
71
+ if (signedDist < -visibleRect.maxDimension / 2 - margin) {
72
+ return {
73
+ path: (0, exports.pathToRenderable)(math_1.Path.fromRect(visibleRect), { fill: renderablePath.style.stroke.color }),
74
+ rectangle: visibleRect,
75
+ fullScreen: true,
76
+ };
77
+ }
78
+ else if (signedDist > visibleRect.maxDimension / 2 + margin) {
79
+ return {
80
+ path: (0, exports.pathToRenderable)(math_1.Path.empty, { fill: math_1.Color4.transparent }),
81
+ rectangle: math_1.Rect2.empty,
82
+ fullScreen: false,
83
+ };
84
+ }
85
+ }
86
+ return null;
87
+ };
88
+ exports.simplifyPathToFullScreenOrEmpty = simplifyPathToFullScreenOrEmpty;
89
+ /**
90
+ * @returns a Path that, when rendered, looks roughly equivalent to the given path.
91
+ */
92
+ const visualEquivalent = (renderablePath, visibleRect) => {
93
+ const path = (0, exports.pathFromRenderable)(renderablePath);
94
+ const strokeWidth = renderablePath.style.stroke?.width ?? 0;
95
+ const onlyStroked = strokeWidth > 0 && renderablePath.style.fill.a === 0;
96
+ const styledPathBBox = path.bbox.grownBy(strokeWidth);
97
+ let rectangleSimplification = (0, exports.simplifyPathToFullScreenOrEmpty)(renderablePath, visibleRect, { fastCheck: true, expensiveCheck: false, });
98
+ if (rectangleSimplification) {
99
+ return rectangleSimplification.path;
100
+ }
44
101
  // Scale the expanded rect --- the visual equivalent is only close for huge strokes.
45
102
  const expandedRect = visibleRect.grownBy(strokeWidth)
46
103
  .transformedBoundingBox(math_1.Mat33.scaling2D(4, visibleRect.center));
47
104
  // TODO: Handle simplifying very small paths.
48
105
  if (expandedRect.containsRect(styledPathBBox)) {
49
- return renderablePath;
106
+ return pathIncluded(renderablePath, path);
50
107
  }
51
108
  const parts = [];
52
109
  let startPoint = path.startPoint;
@@ -81,6 +138,12 @@ const visualEquivalent = (renderablePath, visibleRect) => {
81
138
  }
82
139
  startPoint = endPoint;
83
140
  }
84
- return (0, exports.pathToRenderable)(new math_1.Path(path.startPoint, parts), renderablePath.style);
141
+ const newPath = new math_1.Path(path.startPoint, parts);
142
+ const newStyle = renderablePath.style;
143
+ rectangleSimplification = (0, exports.simplifyPathToFullScreenOrEmpty)(renderablePath, visibleRect, { fastCheck: false, expensiveCheck: true, });
144
+ if (rectangleSimplification) {
145
+ return rectangleSimplification.path;
146
+ }
147
+ return (0, exports.pathToRenderable)(newPath, newStyle);
85
148
  };
86
149
  exports.visualEquivalent = visualEquivalent;
@@ -49,9 +49,11 @@ class CacheRecord {
49
49
  return transform;
50
50
  }
51
51
  setRenderingRegion(drawTo) {
52
- this.renderer.setTransform(
53
- // Invert to map objects instead of the viewport
54
- this.getTransform(drawTo));
52
+ const transform = this.getTransform(drawTo);
53
+ this.renderer.setTransform(transform);
54
+ // The visible region may be slightly larger than where we're actually drawing
55
+ // to (because of rounding).
56
+ this.renderer.overrideVisibleRect(drawTo.grownBy(1 / transform.getScaleFactor()));
55
57
  }
56
58
  }
57
59
  exports.default = CacheRecord;
@@ -5,7 +5,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.CacheRecordManager = void 0;
7
7
  const CacheRecord_1 = __importDefault(require("./CacheRecord"));
8
- const debugMode = false;
9
8
  class CacheRecordManager {
10
9
  constructor(cacheProps) {
11
10
  // Fixed-size array: Cache blocks are assigned indicies into [cachedCanvases].
@@ -22,19 +21,19 @@ class CacheRecordManager {
22
21
  const record = new CacheRecord_1.default(onDealloc, this.cacheState);
23
22
  record.setRenderingRegion(drawTo);
24
23
  this.cacheRecords.push(record);
25
- if (debugMode) {
24
+ if (this.cacheState.debugMode) {
26
25
  console.log('[Cache] Cache spaces used: ', this.cacheRecords.length, ' of ', this.maxCanvases);
27
26
  }
28
27
  return record;
29
28
  }
30
29
  else {
31
30
  const lru = this.getLeastRecentlyUsedRecord();
32
- if (debugMode) {
31
+ if (this.cacheState.debugMode) {
33
32
  console.log('[Cache] Re-alloc. Times allocated: ', lru.allocCount, '\nLast used cycle: ', lru.getLastUsedCycle(), '\nCurrent cycle: ', this.cacheState.currentRenderingCycle);
34
33
  }
35
34
  lru.realloc(onDealloc);
36
35
  lru.setRenderingRegion(drawTo);
37
- if (debugMode) {
36
+ if (this.cacheState.debugMode) {
38
37
  console.log('[Cache] Now re-alloc\'d. Last used cycle: ', lru.getLastUsedCycle());
39
38
  console.assert(lru['cacheState'] === this.cacheState, '[Cache] Unequal cache states! cacheState should be a shared object!');
40
39
  }
@@ -9,4 +9,5 @@ export default class RenderingCache {
9
9
  constructor(cacheProps: CacheProps);
10
10
  render(screenRenderer: AbstractRenderer, image: ImageNode, viewport: Viewport): void;
11
11
  getDebugInfo(): string;
12
+ setIsDebugMode(debugMode: boolean): void;
12
13
  }
@@ -13,6 +13,7 @@ class RenderingCache {
13
13
  props: cacheProps,
14
14
  currentRenderingCycle: 0,
15
15
  recordManager: this.recordManager,
16
+ debugMode: false,
16
17
  };
17
18
  this.recordManager.setSharedState(this.sharedState);
18
19
  }
@@ -49,5 +50,8 @@ class RenderingCache {
49
50
  getDebugInfo() {
50
51
  return this.recordManager.getDebugInfo();
51
52
  }
53
+ setIsDebugMode(debugMode) {
54
+ this.sharedState.debugMode = debugMode;
55
+ }
52
56
  }
53
57
  exports.default = RenderingCache;
@@ -5,8 +5,6 @@ const EditorImage_1 = require("../../image/EditorImage");
5
5
  const math_1 = require("@js-draw/math");
6
6
  // 3x3 divisions for each node.
7
7
  const cacheDivisionSize = 3;
8
- // True: Show rendering updates.
9
- const debugMode = false;
10
8
  class RenderingCacheNode {
11
9
  constructor(region, cacheState) {
12
10
  this.region = region;
@@ -163,8 +161,8 @@ class RenderingCacheNode {
163
161
  items.forEach(item => item.render(screenRenderer, viewport.visibleRect));
164
162
  return;
165
163
  }
166
- if (debugMode) {
167
- screenRenderer.drawRect(this.region, 0.5 * viewport.getSizeOfPixelOnCanvas(), { fill: math_1.Color4.yellow });
164
+ if (this.cacheState.debugMode) {
165
+ screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: math_1.Color4.yellow });
168
166
  }
169
167
  // Could we render direclty from [this] or do we need to recurse?
170
168
  const couldRender = this.renderingWouldBeHighEnoughResolution(viewport);
@@ -199,7 +197,9 @@ class RenderingCacheNode {
199
197
  }
200
198
  let leafApproxRenderTime = 0;
201
199
  for (const leaf of leavesByIds) {
202
- leafApproxRenderTime += leaf.getContent().getProportionalRenderingTime();
200
+ if (!tooSmallToRender(leaf.getBBox())) {
201
+ leafApproxRenderTime += leaf.getContent().getProportionalRenderingTime();
202
+ }
203
203
  }
204
204
  // Is it worth it to render the items?
205
205
  if (leafApproxRenderTime > this.cacheState.props.minProportionalRenderTimePerCache) {
@@ -235,26 +235,30 @@ class RenderingCacheNode {
235
235
  this.renderedMaxZIndex = zIndex;
236
236
  }
237
237
  }
238
- if (debugMode) {
239
- screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: math_1.Color4.clay });
238
+ if (this.cacheState.debugMode) {
239
+ // Clay for adding new elements
240
+ screenRenderer.drawRect(this.region, 2 * viewport.getSizeOfPixelOnCanvas(), { fill: math_1.Color4.clay });
240
241
  }
241
242
  }
242
243
  }
243
- else if (debugMode) {
244
+ else if (this.cacheState.debugMode) {
244
245
  console.log('Decided on a full re-render. Reason: At least one of the following is false:', '\n leafIds.length > this.renderedIds.length: ', leafIds.length > this.renderedIds.length, '\n this.allRenderedIdsIn(leafIds): ', this.allRenderedIdsIn(leafIds), '\n this.renderedMaxZIndex !== null: ', this.renderedMaxZIndex !== null, '\n\nthis.rerenderedIds: ', this.renderedIds, ', leafIds: ', leafIds);
245
246
  }
246
247
  if (fullRerenderNeeded) {
247
248
  thisRenderer = this.cachedRenderer.startRender();
248
249
  thisRenderer.clear();
249
250
  this.renderedMaxZIndex = null;
250
- for (const leaf of leaves) {
251
+ const startIndex = (0, EditorImage_1.computeFirstIndexToRender)(leaves, this.region);
252
+ for (let i = startIndex; i < leaves.length; i++) {
253
+ const leaf = leaves[i];
251
254
  const content = leaf.getContent();
252
255
  this.renderedMaxZIndex ??= content.getZIndex();
253
256
  this.renderedMaxZIndex = Math.max(this.renderedMaxZIndex, content.getZIndex());
254
257
  leaf.render(thisRenderer, this.region);
255
258
  }
256
- if (debugMode) {
257
- screenRenderer.drawRect(this.region, viewport.getSizeOfPixelOnCanvas(), { fill: math_1.Color4.red });
259
+ if (this.cacheState.debugMode) {
260
+ // Red for full rerender
261
+ screenRenderer.drawRect(this.region, 3 * viewport.getSizeOfPixelOnCanvas(), { fill: math_1.Color4.red });
258
262
  }
259
263
  }
260
264
  this.renderedIds = leafIds;
@@ -271,6 +275,10 @@ class RenderingCacheNode {
271
275
  leaf.render(screenRenderer, this.region.intersection(viewport.visibleRect));
272
276
  }
273
277
  screenRenderer.endObject();
278
+ if (this.cacheState.debugMode) {
279
+ // Green for no cache needed render
280
+ screenRenderer.drawRect(this.region, 2 * viewport.getSizeOfPixelOnCanvas(), { fill: math_1.Color4.green });
281
+ }
274
282
  }
275
283
  }
276
284
  else {
@@ -16,4 +16,5 @@ export interface CacheState {
16
16
  currentRenderingCycle: number;
17
17
  props: CacheProps;
18
18
  recordManager: CacheRecordManager;
19
+ debugMode: boolean;
19
20
  }
@@ -20,6 +20,11 @@ export default abstract class AbstractRenderer {
20
20
  private selfTransform;
21
21
  private transformStack;
22
22
  protected constructor(viewport: Viewport);
23
+ /**
24
+ * this.canvasToScreen, etc. should be used instead of the corresponding
25
+ * methods on `Viewport`, because the viewport may not accurately reflect
26
+ * what is rendered.
27
+ */
23
28
  protected getViewport(): Viewport;
24
29
  abstract displaySize(): Vec2;
25
30
  abstract clear(): void;
@@ -73,5 +78,10 @@ export default abstract class AbstractRenderer {
73
78
  getCanvasToScreenTransform(): Mat33;
74
79
  canvasToScreen(vec: Vec2): Vec2;
75
80
  getSizeOfCanvasPixelOnScreen(): number;
81
+ private visibleRectOverride;
82
+ /**
83
+ * @internal
84
+ */
85
+ overrideVisibleRect(rect: Rect2 | null): void;
76
86
  getVisibleRect(): Rect2;
77
87
  }
@@ -17,8 +17,11 @@ class AbstractRenderer {
17
17
  this.objectLevel = 0;
18
18
  this.currentPaths = null;
19
19
  }
20
- // this.canvasToScreen, etc. should be used instead of the corresponding
21
- // methods on Viewport.
20
+ /**
21
+ * this.canvasToScreen, etc. should be used instead of the corresponding
22
+ * methods on `Viewport`, because the viewport may not accurately reflect
23
+ * what is rendered.
24
+ */
22
25
  getViewport() { return this.viewport; }
23
26
  setDraftMode(_draftMode) { }
24
27
  flushPath() {
@@ -160,13 +163,19 @@ class AbstractRenderer {
160
163
  getSizeOfCanvasPixelOnScreen() {
161
164
  return this.getCanvasToScreenTransform().transformVec3(math_1.Vec2.unitX).length();
162
165
  }
166
+ /**
167
+ * @internal
168
+ */
169
+ overrideVisibleRect(rect) {
170
+ this.visibleRectOverride = rect;
171
+ }
163
172
  // Returns the region in canvas space that is visible within the viewport this
164
173
  // canvas is rendering to.
165
174
  //
166
175
  // Note that in some cases this might not be the same as the `visibleRect` given
167
176
  // to components in their `render` method.
168
177
  getVisibleRect() {
169
- return this.viewport.visibleRect;
178
+ return this.visibleRectOverride ?? this.viewport.visibleRect;
170
179
  }
171
180
  }
172
181
  exports.default = AbstractRenderer;
@@ -82,7 +82,10 @@ class CanvasRenderer extends AbstractRenderer_1.default {
82
82
  return math_1.Vec2.of(this.ctx.canvas.clientWidth, this.ctx.canvas.clientHeight);
83
83
  }
84
84
  clear() {
85
+ this.ctx.save();
86
+ this.ctx.resetTransform();
85
87
  this.ctx.clearRect(0, 0, this.ctx.canvas.width, this.ctx.canvas.height);
88
+ this.ctx.restore();
86
89
  }
87
90
  beginPath(startPoint) {
88
91
  startPoint = this.canvasToScreen(startPoint);
@@ -149,7 +152,7 @@ class CanvasRenderer extends AbstractRenderer_1.default {
149
152
  return;
150
153
  }
151
154
  // If part of a huge object, it might be worth trimming the path
152
- const visibleRect = this.getViewport().visibleRect;
155
+ const visibleRect = this.getVisibleRect();
153
156
  if (this.currentObjectBBox?.containsRect(visibleRect)) {
154
157
  // Try to trim/remove parts of the path outside of the bounding box.
155
158
  path = (0, RenderablePathSpec_1.visualEquivalent)(path, visibleRect);
@@ -189,7 +192,7 @@ class CanvasRenderer extends AbstractRenderer_1.default {
189
192
  if (!this.ignoringObject && clip) {
190
193
  // Don't clip if it would only remove content already trimmed by
191
194
  // the edge of the screen.
192
- const clippedIsOutsideScreen = boundingBox.containsRect(this.getViewport().visibleRect);
195
+ const clippedIsOutsideScreen = boundingBox.containsRect(this.getVisibleRect());
193
196
  if (!clippedIsOutsideScreen) {
194
197
  this.clipLevels.push(this.objectLevel);
195
198
  this.ctx.save();
@@ -59,16 +59,6 @@ export default class SVGRenderer extends AbstractRenderer {
59
59
  drawPoints(...points: Point2[]): void;
60
60
  drawSVGElem(elem: SVGElement): void;
61
61
  isTooSmallToRender(_rect: Rect2): boolean;
62
- private visibleRectOverride;
63
- /**
64
- * Overrides the visible region returned by `getVisibleRect`.
65
- *
66
- * This is useful when the `viewport`'s transform has been modified,
67
- * for example, to compensate for storing part of the image's
68
- * transformation in an SVG property.
69
- */
70
- private overrideVisibleRect;
71
- getVisibleRect(): Rect2;
72
62
  /**
73
63
  * Creates a new SVG element and `SVGRenerer` with `width`, `height`, `viewBox`,
74
64
  * and other metadata attributes set for the given `Viewport`.
@@ -42,7 +42,6 @@ class SVGRenderer extends AbstractRenderer_1.default {
42
42
  this.textContainer = null;
43
43
  this.textContainerTransform = null;
44
44
  this.textParentStyle = defaultTextStyle;
45
- this.visibleRectOverride = null;
46
45
  this.clear();
47
46
  this.addStyleSheet();
48
47
  }
@@ -345,19 +344,6 @@ class SVGRenderer extends AbstractRenderer_1.default {
345
344
  isTooSmallToRender(_rect) {
346
345
  return false;
347
346
  }
348
- /**
349
- * Overrides the visible region returned by `getVisibleRect`.
350
- *
351
- * This is useful when the `viewport`'s transform has been modified,
352
- * for example, to compensate for storing part of the image's
353
- * transformation in an SVG property.
354
- */
355
- overrideVisibleRect(newRect) {
356
- this.visibleRectOverride = newRect;
357
- }
358
- getVisibleRect() {
359
- return this.visibleRectOverride ?? super.getVisibleRect();
360
- }
361
347
  /**
362
348
  * Creates a new SVG element and `SVGRenerer` with `width`, `height`, `viewBox`,
363
349
  * and other metadata attributes set for the given `Viewport`.
@@ -0,0 +1,14 @@
1
+ import type Editor from '../Editor';
2
+ import { Point2 } from '@js-draw/math';
3
+ /**
4
+ * Creates two pointers and sends the touch {@link InputEvtType.PointerDownEvt}s for them.
5
+ *
6
+ * Returns an object that allows continuing or ending the gesture.
7
+ *
8
+ * `initialRotation` should be in radians.
9
+ */
10
+ declare const startPinchGesture: (editor: Editor, center: Point2, initialDistance: number, initialRotation: number) => {
11
+ update(center: Point2, distance: number, rotation: number): void;
12
+ end(): void;
13
+ };
14
+ export default startPinchGesture;
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const math_1 = require("@js-draw/math");
7
+ const sendTouchEvent_1 = __importDefault(require("./sendTouchEvent"));
8
+ const inputEvents_1 = require("../inputEvents");
9
+ /**
10
+ * Creates two pointers and sends the touch {@link InputEvtType.PointerDownEvt}s for them.
11
+ *
12
+ * Returns an object that allows continuing or ending the gesture.
13
+ *
14
+ * `initialRotation` should be in radians.
15
+ */
16
+ const startPinchGesture = (editor, center, initialDistance, initialRotation) => {
17
+ const computeTouchPoints = (center, distance, rotation) => {
18
+ const halfDisplacement = math_1.Mat33.zRotation(rotation).transformVec2(math_1.Vec2.of(0, distance / 2));
19
+ const point1 = center.plus(halfDisplacement);
20
+ const point2 = center.minus(halfDisplacement);
21
+ return [point1, point2];
22
+ };
23
+ let [touchPoint1, touchPoint2] = computeTouchPoints(center, initialDistance, initialRotation);
24
+ let firstPointer = (0, sendTouchEvent_1.default)(editor, inputEvents_1.InputEvtType.PointerDownEvt, touchPoint1);
25
+ let secondPointer = (0, sendTouchEvent_1.default)(editor, inputEvents_1.InputEvtType.PointerDownEvt, touchPoint2, [firstPointer]);
26
+ return {
27
+ update(center, distance, rotation) {
28
+ const eventType = inputEvents_1.InputEvtType.PointerMoveEvt;
29
+ const [newPoint1, newPoint2] = computeTouchPoints(center, distance, rotation);
30
+ touchPoint1 = newPoint1;
31
+ touchPoint2 = newPoint2;
32
+ firstPointer = (0, sendTouchEvent_1.default)(editor, eventType, newPoint1, [secondPointer]);
33
+ secondPointer = (0, sendTouchEvent_1.default)(editor, eventType, newPoint2, [firstPointer]);
34
+ },
35
+ end() {
36
+ (0, sendTouchEvent_1.default)(editor, inputEvents_1.InputEvtType.PointerUpEvt, touchPoint1, [secondPointer]);
37
+ (0, sendTouchEvent_1.default)(editor, inputEvents_1.InputEvtType.PointerUpEvt, touchPoint2);
38
+ },
39
+ };
40
+ };
41
+ exports.default = startPinchGesture;
@@ -21,6 +21,9 @@ export default class PanZoom extends BaseTool {
21
21
  private editor;
22
22
  private mode;
23
23
  private transform;
24
+ private readonly initialRotationSnapAngle;
25
+ private readonly afterRotationStartSnapAngle;
26
+ private readonly pinchZoomStartThreshold;
24
27
  private startDist;
25
28
  private lastDist;
26
29
  private lastScreenCenter;
@@ -29,6 +32,7 @@ export default class PanZoom extends BaseTool {
29
32
  private initialTouchAngle;
30
33
  private initialViewportRotation;
31
34
  private isScaling;
35
+ private isRotating;
32
36
  private inertialScroller;
33
37
  private velocity;
34
38
  constructor(editor: Editor, mode: PanZoomMode, description: string);