js-draw 1.8.0 → 1.9.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.
Files changed (35) hide show
  1. package/LICENSE +1 -1
  2. package/dist/bundle.js +2 -2
  3. package/dist/cjs/Editor.js +24 -13
  4. package/dist/cjs/SVGLoader.d.ts +1 -1
  5. package/dist/cjs/SVGLoader.js +11 -2
  6. package/dist/cjs/Viewport.d.ts +6 -0
  7. package/dist/cjs/Viewport.js +6 -1
  8. package/dist/cjs/image/EditorImage.d.ts +20 -0
  9. package/dist/cjs/image/EditorImage.js +40 -11
  10. package/dist/cjs/rendering/Display.d.ts +9 -0
  11. package/dist/cjs/rendering/Display.js +47 -6
  12. package/dist/cjs/rendering/caching/CacheRecordManager.js +3 -4
  13. package/dist/cjs/rendering/caching/RenderingCache.d.ts +1 -0
  14. package/dist/cjs/rendering/caching/RenderingCache.js +4 -0
  15. package/dist/cjs/rendering/caching/RenderingCacheNode.js +19 -11
  16. package/dist/cjs/rendering/caching/types.d.ts +1 -0
  17. package/dist/cjs/rendering/renderers/CanvasRenderer.js +3 -0
  18. package/dist/cjs/version.js +1 -1
  19. package/dist/mjs/Editor.mjs +24 -13
  20. package/dist/mjs/SVGLoader.d.ts +1 -1
  21. package/dist/mjs/SVGLoader.mjs +11 -2
  22. package/dist/mjs/Viewport.d.ts +6 -0
  23. package/dist/mjs/Viewport.mjs +6 -1
  24. package/dist/mjs/image/EditorImage.d.ts +20 -0
  25. package/dist/mjs/image/EditorImage.mjs +38 -10
  26. package/dist/mjs/rendering/Display.d.ts +9 -0
  27. package/dist/mjs/rendering/Display.mjs +47 -6
  28. package/dist/mjs/rendering/caching/CacheRecordManager.mjs +3 -4
  29. package/dist/mjs/rendering/caching/RenderingCache.d.ts +1 -0
  30. package/dist/mjs/rendering/caching/RenderingCache.mjs +4 -0
  31. package/dist/mjs/rendering/caching/RenderingCacheNode.mjs +20 -12
  32. package/dist/mjs/rendering/caching/types.d.ts +1 -0
  33. package/dist/mjs/rendering/renderers/CanvasRenderer.mjs +3 -0
  34. package/dist/mjs/version.mjs +1 -1
  35. package/package.json +4 -4
@@ -189,19 +189,30 @@ class Editor {
189
189
  this.hideLoadingWarning();
190
190
  // Enforce zoom limits.
191
191
  this.notifier.on(types_1.EditorEventType.ViewportChanged, evt => {
192
- if (evt.kind === types_1.EditorEventType.ViewportChanged) {
193
- const zoom = evt.newTransform.transformVec3(math_1.Vec2.unitX).length();
194
- if (zoom > this.settings.maxZoom || zoom < this.settings.minZoom) {
195
- const oldZoom = evt.oldTransform.transformVec3(math_1.Vec2.unitX).length();
196
- let resetTransform = math_1.Mat33.identity;
197
- if (oldZoom <= this.settings.maxZoom && oldZoom >= this.settings.minZoom) {
198
- resetTransform = evt.oldTransform;
199
- }
200
- else {
201
- // If 1x zoom isn't acceptable, try a zoom between the minimum and maximum.
202
- resetTransform = math_1.Mat33.scaling2D((this.settings.minZoom + this.settings.maxZoom) / 2);
203
- }
204
- this.viewport.resetTransform(resetTransform);
192
+ if (evt.kind !== types_1.EditorEventType.ViewportChanged)
193
+ return;
194
+ const getZoom = (mat) => mat.transformVec3(math_1.Vec2.unitX).length();
195
+ const zoom = getZoom(evt.newTransform);
196
+ if (zoom > this.settings.maxZoom || zoom < this.settings.minZoom) {
197
+ const oldZoom = getZoom(evt.oldTransform);
198
+ let resetTransform = math_1.Mat33.identity;
199
+ if (oldZoom <= this.settings.maxZoom && oldZoom >= this.settings.minZoom) {
200
+ resetTransform = evt.oldTransform;
201
+ }
202
+ else {
203
+ // If 1x zoom isn't acceptable, try a zoom between the minimum and maximum.
204
+ resetTransform = math_1.Mat33.scaling2D((this.settings.minZoom + this.settings.maxZoom) / 2);
205
+ }
206
+ this.viewport.resetTransform(resetTransform);
207
+ }
208
+ else if (!isFinite(zoom)) {
209
+ // Recover from possible division-by-zero
210
+ console.warn(`Non-finite zoom (${zoom}) detected. Resetting the viewport. This was likely caused by division by zero.`);
211
+ if (isFinite(getZoom(evt.oldTransform))) {
212
+ this.viewport.resetTransform(evt.oldTransform);
213
+ }
214
+ else {
215
+ this.viewport.resetTransform();
205
216
  }
206
217
  }
207
218
  });
@@ -18,7 +18,7 @@ export declare enum SVGLoaderLoadMethod {
18
18
  export interface SVGLoaderOptions {
19
19
  sanitize?: boolean;
20
20
  disableUnknownObjectWarnings?: boolean;
21
- loadMethod?: 'iframe' | 'domparser';
21
+ loadMethod?: SVGLoaderLoadMethod;
22
22
  }
23
23
  export default class SVGLoader implements ImageLoader {
24
24
  private source;
@@ -35,6 +35,7 @@ const SVGGlobalAttributesObject_1 = __importDefault(require("./components/SVGGlo
35
35
  const TextComponent_1 = __importStar(require("./components/TextComponent"));
36
36
  const UnknownSVGObject_1 = __importDefault(require("./components/UnknownSVGObject"));
37
37
  const RenderablePathSpec_1 = require("./rendering/RenderablePathSpec");
38
+ const SVGRenderer_1 = require("./rendering/renderers/SVGRenderer");
38
39
  // Size of a loaded image if no size is specified.
39
40
  exports.defaultSVGViewRect = new math_1.Rect2(0, 0, 500, 500);
40
41
  // Key to retrieve unrecognised attributes from an AbstractComponent
@@ -504,7 +505,13 @@ class SVGLoader {
504
505
  this.updateSVGAttrs(node);
505
506
  break;
506
507
  case 'style':
507
- await this.addUnknownNode(node);
508
+ // Keeping unnecessary style sheets can cause the browser to keep all
509
+ // SVG elements *referenced* by the style sheet in some browsers.
510
+ //
511
+ // Only keep the style sheet if it won't be discarded on save.
512
+ if (node.getAttribute('id') !== SVGRenderer_1.renderedStylesheetId) {
513
+ await this.addUnknownNode(node);
514
+ }
508
515
  break;
509
516
  default:
510
517
  if (!this.disableUnknownObjectWarnings) {
@@ -547,6 +554,7 @@ class SVGLoader {
547
554
  this.onDetermineExportRect?.(exports.defaultSVGViewRect);
548
555
  }
549
556
  this.onFinish?.();
557
+ this.onFinish = null;
550
558
  }
551
559
  /**
552
560
  * Create an `SVGLoader` from the content of an SVG image. SVGs are loaded within a sandboxed
@@ -558,7 +566,7 @@ class SVGLoader {
558
566
  * @param options - if `true` or `false`, treated as the `sanitize` option -- don't store unknown attributes.
559
567
  */
560
568
  static fromString(text, options = false) {
561
- const domParserLoad = typeof options !== 'boolean' && options?.loadMethod === 'domparser';
569
+ const domParserLoad = typeof options !== 'boolean' && options?.loadMethod === SVGLoaderLoadMethod.DOMParser;
562
570
  const { svgElem, cleanUp } = (() => {
563
571
  // If the user requested an iframe load (the default) try to load with an iframe.
564
572
  // There are some cases (e.g. in a sandboxed iframe) where this doesn't work.
@@ -606,6 +614,7 @@ class SVGLoader {
606
614
  const cleanUp = () => {
607
615
  svgElem.remove();
608
616
  sandbox.remove();
617
+ sandbox.src = '';
609
618
  };
610
619
  return { svgElem, cleanUp };
611
620
  }
@@ -50,6 +50,11 @@ export declare class Viewport {
50
50
  private getScaleFactorToNearestPowerOf;
51
51
  /** Returns the size of a grid cell (in canvas units) as used by {@link snapToGrid}. */
52
52
  static getGridSize(scaleFactor: number): number;
53
+ /**
54
+ * Snaps `canvasPos` to the nearest grid cell corner.
55
+ *
56
+ * @see {@link getGridSize} and {@link getScaleFactorToNearestPowerOf}.
57
+ */
53
58
  snapToGrid(canvasPos: Point2): Vec3;
54
59
  /** Returns the size of one screen pixel in canvas units. */
55
60
  getSizeOfPixelOnCanvas(): number;
@@ -65,6 +70,7 @@ export declare class Viewport {
65
70
  * its original location. This is useful for preparing data for base-10 conversion.
66
71
  */
67
72
  static roundPoint<T extends Point2 | number>(point: T, tolerance: number): PointDataType<T>;
73
+ /** Round a point with a tolerance of ±1 screen unit. */
68
74
  roundPoint(point: Point2): Point2;
69
75
  static roundScaleRatio(scaleRatio: number, roundAmount?: number): number;
70
76
  computeZoomToTransform(toMakeVisible: Rect2, allowZoomIn?: boolean, allowZoomOut?: boolean): Mat33;
@@ -104,6 +104,11 @@ class Viewport {
104
104
  static getGridSize(scaleFactor) {
105
105
  return 50 / scaleFactor;
106
106
  }
107
+ /**
108
+ * Snaps `canvasPos` to the nearest grid cell corner.
109
+ *
110
+ * @see {@link getGridSize} and {@link getScaleFactorToNearestPowerOf}.
111
+ */
107
112
  snapToGrid(canvasPos) {
108
113
  const scaleFactor = this.getScaleFactorToNearestPowerOf(2);
109
114
  const snapCoordinate = (coordinate) => {
@@ -140,7 +145,7 @@ class Viewport {
140
145
  }
141
146
  return point.map(roundComponent);
142
147
  }
143
- // Round a point with a tolerance of ±1 screen unit.
148
+ /** Round a point with a tolerance of ±1 screen unit. */
144
149
  roundPoint(point) {
145
150
  return Viewport.roundPoint(point, 1 / this.getScaleFactor());
146
151
  }
@@ -123,8 +123,28 @@ export default class EditorImage {
123
123
  */
124
124
  private setExportRectDirectly;
125
125
  private onExportViewportChanged;
126
+ /**
127
+ * @internal
128
+ *
129
+ * Enables debug mode for **all** `EditorImage`s.
130
+ *
131
+ * **Only use for debugging**.
132
+ *
133
+ * @internal
134
+ */
135
+ setDebugMode(newDebugMode: boolean): void;
126
136
  private static SetImportExportRectCommand;
127
137
  }
138
+ /**
139
+ * Determines the first index in `sortedLeaves` that needs to be rendered
140
+ * (based on occlusion -- everything before that index can be skipped and
141
+ * produce a visually-equivalent image).
142
+ *
143
+ * Does nothing if visibleRect is not provided
144
+ *
145
+ * @internal
146
+ */
147
+ export declare const computeFirstIndexToRender: (sortedLeaves: Array<ImageNode>, visibleRect?: Rect2) => number;
128
148
  type TooSmallToRenderCheck = (rect: Rect2) => boolean;
129
149
  /**
130
150
  * Part of the Editor's image. Does not handle fullscreen/invisible components.
@@ -31,7 +31,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
31
31
  };
32
32
  var _a, _b, _c;
33
33
  Object.defineProperty(exports, "__esModule", { value: true });
34
- exports.RootImageNode = exports.ImageNode = exports.EditorImageEventType = exports.sortLeavesByZIndex = void 0;
34
+ exports.RootImageNode = exports.ImageNode = exports.computeFirstIndexToRender = exports.EditorImageEventType = exports.sortLeavesByZIndex = void 0;
35
35
  const Viewport_1 = __importDefault(require("../Viewport"));
36
36
  const AbstractComponent_1 = __importStar(require("../components/AbstractComponent"));
37
37
  const math_1 = require("@js-draw/math");
@@ -49,7 +49,7 @@ var EditorImageEventType;
49
49
  EditorImageEventType[EditorImageEventType["ExportViewportChanged"] = 0] = "ExportViewportChanged";
50
50
  EditorImageEventType[EditorImageEventType["AutoresizeModeChanged"] = 1] = "AutoresizeModeChanged";
51
51
  })(EditorImageEventType || (exports.EditorImageEventType = EditorImageEventType = {}));
52
- const debugMode = false;
52
+ let debugMode = false;
53
53
  // Handles lookup/storage of elements in the image
54
54
  class EditorImage {
55
55
  // @internal
@@ -301,6 +301,18 @@ class EditorImage {
301
301
  });
302
302
  }
303
303
  }
304
+ /**
305
+ * @internal
306
+ *
307
+ * Enables debug mode for **all** `EditorImage`s.
308
+ *
309
+ * **Only use for debugging**.
310
+ *
311
+ * @internal
312
+ */
313
+ setDebugMode(newDebugMode) {
314
+ debugMode = newDebugMode;
315
+ }
304
316
  }
305
317
  _a = EditorImage;
306
318
  // A Command that can access private [EditorImage] functionality
@@ -440,6 +452,31 @@ EditorImage.SetImportExportRectCommand = (_c = class extends SerializableCommand
440
452
  })(),
441
453
  _c);
442
454
  exports.default = EditorImage;
455
+ /**
456
+ * Determines the first index in `sortedLeaves` that needs to be rendered
457
+ * (based on occlusion -- everything before that index can be skipped and
458
+ * produce a visually-equivalent image).
459
+ *
460
+ * Does nothing if visibleRect is not provided
461
+ *
462
+ * @internal
463
+ */
464
+ const computeFirstIndexToRender = (sortedLeaves, visibleRect) => {
465
+ let startIndex = 0;
466
+ if (visibleRect) {
467
+ for (let i = sortedLeaves.length - 1; i >= 1; i--) {
468
+ if (
469
+ // Check for occlusion
470
+ sortedLeaves[i].getBBox().containsRect(visibleRect)
471
+ && sortedLeaves[i].getContent()?.occludesEverythingBelowWhenRenderedInRect(visibleRect)) {
472
+ startIndex = i;
473
+ break;
474
+ }
475
+ }
476
+ }
477
+ return startIndex;
478
+ };
479
+ exports.computeFirstIndexToRender = computeFirstIndexToRender;
443
480
  /**
444
481
  * Part of the Editor's image. Does not handle fullscreen/invisible components.
445
482
  * @internal
@@ -696,15 +733,7 @@ class ImageNode {
696
733
  // If some components hide others (and we're permitted to simplify,
697
734
  // which is true in the case of visibleRect being defined), then only
698
735
  // draw the non-hidden components:
699
- let startIndex = 0;
700
- if (visibleRect) {
701
- for (let i = leaves.length - 1; i >= 1; i--) {
702
- if (leaves[i].getContent()?.occludesEverythingBelowWhenRenderedInRect(visibleRect)) {
703
- startIndex = i;
704
- break;
705
- }
706
- }
707
- }
736
+ const startIndex = (0, exports.computeFirstIndexToRender)(leaves);
708
737
  for (let i = startIndex; i < leaves.length; i++) {
709
738
  const leaf = leaves[i];
710
739
  // Leaves by definition have content
@@ -26,6 +26,7 @@ export default class Display {
26
26
  private textRenderer;
27
27
  private textRerenderOutput;
28
28
  private cache;
29
+ private devicePixelRatio;
29
30
  private resizeSurfacesCallback?;
30
31
  private flattenCallback?;
31
32
  /** @internal */
@@ -46,6 +47,14 @@ export default class Display {
46
47
  getColorAt: (_screenPos: Point2) => Color4 | null;
47
48
  private initializeCanvasRendering;
48
49
  private initializeTextRendering;
50
+ /**
51
+ * Sets the device-pixel-ratio.
52
+ *
53
+ * Intended for debugging. Users do not need to call this manually.
54
+ *
55
+ * @internal
56
+ */
57
+ setDevicePixelRatio(dpr: number): Promise<void> | undefined;
49
58
  /**
50
59
  * Rerenders the text-based display.
51
60
  * The text-based display is intended for screen readers and can be navigated to by pressing `tab`.
@@ -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,32 @@ 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
+ //
142
+ // This scaling causes the rendering contexts to automatically convert
143
+ // between screen coordinates and pixel coordinates.
144
+ wetInkCtx.resetTransform();
145
+ dryInkCtx.resetTransform();
146
+ dryInkCtx.scale(this.devicePixelRatio, this.devicePixelRatio);
147
+ wetInkCtx.scale(this.devicePixelRatio, this.devicePixelRatio);
131
148
  this.editor.notifier.dispatch(types_1.EditorEventType.DisplayResized, {
132
149
  kind: types_1.EditorEventType.DisplayResized,
133
150
  newSize: math_1.Vec2.of(this.width, this.height),
@@ -136,10 +153,17 @@ class Display {
136
153
  };
137
154
  this.resizeSurfacesCallback();
138
155
  this.flattenCallback = () => {
156
+ dryInkCtx.save();
157
+ dryInkCtx.resetTransform();
139
158
  dryInkCtx.drawImage(wetInkCanvas, 0, 0);
159
+ dryInkCtx.restore();
140
160
  };
141
161
  this.getColorAt = (screenPos) => {
142
- const pixel = dryInkCtx.getImageData(screenPos.x, screenPos.y, 1, 1);
162
+ // getImageData isn't affected by a transformation matrix -- we need to
163
+ // pre-transform screenPos to convert it from screen coordinates into pixel
164
+ // coordinates.
165
+ const adjustedScreenPos = screenPos.times(this.devicePixelRatio);
166
+ const pixel = dryInkCtx.getImageData(adjustedScreenPos.x, adjustedScreenPos.y, 1, 1);
143
167
  const data = pixel?.data;
144
168
  if (data) {
145
169
  const color = math_1.Color4.ofRGBA(data[0] / 255, data[1] / 255, data[2] / 255, data[3] / 255);
@@ -162,6 +186,23 @@ class Display {
162
186
  textRendererOutputContainer.replaceChildren(rerenderButton, this.textRerenderOutput);
163
187
  this.editor.createHTMLOverlay(textRendererOutputContainer);
164
188
  }
189
+ /**
190
+ * Sets the device-pixel-ratio.
191
+ *
192
+ * Intended for debugging. Users do not need to call this manually.
193
+ *
194
+ * @internal
195
+ */
196
+ setDevicePixelRatio(dpr) {
197
+ const minDpr = 0.001;
198
+ const maxDpr = 10;
199
+ if (isFinite(dpr) && dpr >= minDpr && dpr <= maxDpr && dpr !== this.devicePixelRatio) {
200
+ this.devicePixelRatio = dpr;
201
+ this.resizeSurfacesCallback?.();
202
+ return this.editor.queueRerender();
203
+ }
204
+ return undefined;
205
+ }
165
206
  /**
166
207
  * Rerenders the text-based display.
167
208
  * The text-based display is intended for screen readers and can be navigated to by pressing `tab`.
@@ -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
  }
@@ -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);
@@ -1,5 +1,5 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = {
4
- number: '1.8.0',
4
+ number: '1.9.1',
5
5
  };
@@ -160,19 +160,30 @@ export class Editor {
160
160
  this.hideLoadingWarning();
161
161
  // Enforce zoom limits.
162
162
  this.notifier.on(EditorEventType.ViewportChanged, evt => {
163
- if (evt.kind === EditorEventType.ViewportChanged) {
164
- const zoom = evt.newTransform.transformVec3(Vec2.unitX).length();
165
- if (zoom > this.settings.maxZoom || zoom < this.settings.minZoom) {
166
- const oldZoom = evt.oldTransform.transformVec3(Vec2.unitX).length();
167
- let resetTransform = Mat33.identity;
168
- if (oldZoom <= this.settings.maxZoom && oldZoom >= this.settings.minZoom) {
169
- resetTransform = evt.oldTransform;
170
- }
171
- else {
172
- // If 1x zoom isn't acceptable, try a zoom between the minimum and maximum.
173
- resetTransform = Mat33.scaling2D((this.settings.minZoom + this.settings.maxZoom) / 2);
174
- }
175
- this.viewport.resetTransform(resetTransform);
163
+ if (evt.kind !== EditorEventType.ViewportChanged)
164
+ return;
165
+ const getZoom = (mat) => mat.transformVec3(Vec2.unitX).length();
166
+ const zoom = getZoom(evt.newTransform);
167
+ if (zoom > this.settings.maxZoom || zoom < this.settings.minZoom) {
168
+ const oldZoom = getZoom(evt.oldTransform);
169
+ let resetTransform = Mat33.identity;
170
+ if (oldZoom <= this.settings.maxZoom && oldZoom >= this.settings.minZoom) {
171
+ resetTransform = evt.oldTransform;
172
+ }
173
+ else {
174
+ // If 1x zoom isn't acceptable, try a zoom between the minimum and maximum.
175
+ resetTransform = Mat33.scaling2D((this.settings.minZoom + this.settings.maxZoom) / 2);
176
+ }
177
+ this.viewport.resetTransform(resetTransform);
178
+ }
179
+ else if (!isFinite(zoom)) {
180
+ // Recover from possible division-by-zero
181
+ console.warn(`Non-finite zoom (${zoom}) detected. Resetting the viewport. This was likely caused by division by zero.`);
182
+ if (isFinite(getZoom(evt.oldTransform))) {
183
+ this.viewport.resetTransform(evt.oldTransform);
184
+ }
185
+ else {
186
+ this.viewport.resetTransform();
176
187
  }
177
188
  }
178
189
  });
@@ -18,7 +18,7 @@ export declare enum SVGLoaderLoadMethod {
18
18
  export interface SVGLoaderOptions {
19
19
  sanitize?: boolean;
20
20
  disableUnknownObjectWarnings?: boolean;
21
- loadMethod?: 'iframe' | 'domparser';
21
+ loadMethod?: SVGLoaderLoadMethod;
22
22
  }
23
23
  export default class SVGLoader implements ImageLoader {
24
24
  private source;
@@ -6,6 +6,7 @@ import SVGGlobalAttributesObject from './components/SVGGlobalAttributesObject.
6
6
  import TextComponent, { TextTransformMode } from './components/TextComponent.mjs';
7
7
  import UnknownSVGObject from './components/UnknownSVGObject.mjs';
8
8
  import { pathToRenderable } from './rendering/RenderablePathSpec.mjs';
9
+ import { renderedStylesheetId } from './rendering/renderers/SVGRenderer.mjs';
9
10
  // Size of a loaded image if no size is specified.
10
11
  export const defaultSVGViewRect = new Rect2(0, 0, 500, 500);
11
12
  // Key to retrieve unrecognised attributes from an AbstractComponent
@@ -475,7 +476,13 @@ export default class SVGLoader {
475
476
  this.updateSVGAttrs(node);
476
477
  break;
477
478
  case 'style':
478
- await this.addUnknownNode(node);
479
+ // Keeping unnecessary style sheets can cause the browser to keep all
480
+ // SVG elements *referenced* by the style sheet in some browsers.
481
+ //
482
+ // Only keep the style sheet if it won't be discarded on save.
483
+ if (node.getAttribute('id') !== renderedStylesheetId) {
484
+ await this.addUnknownNode(node);
485
+ }
479
486
  break;
480
487
  default:
481
488
  if (!this.disableUnknownObjectWarnings) {
@@ -518,6 +525,7 @@ export default class SVGLoader {
518
525
  this.onDetermineExportRect?.(defaultSVGViewRect);
519
526
  }
520
527
  this.onFinish?.();
528
+ this.onFinish = null;
521
529
  }
522
530
  /**
523
531
  * Create an `SVGLoader` from the content of an SVG image. SVGs are loaded within a sandboxed
@@ -529,7 +537,7 @@ export default class SVGLoader {
529
537
  * @param options - if `true` or `false`, treated as the `sanitize` option -- don't store unknown attributes.
530
538
  */
531
539
  static fromString(text, options = false) {
532
- const domParserLoad = typeof options !== 'boolean' && options?.loadMethod === 'domparser';
540
+ const domParserLoad = typeof options !== 'boolean' && options?.loadMethod === SVGLoaderLoadMethod.DOMParser;
533
541
  const { svgElem, cleanUp } = (() => {
534
542
  // If the user requested an iframe load (the default) try to load with an iframe.
535
543
  // There are some cases (e.g. in a sandboxed iframe) where this doesn't work.
@@ -577,6 +585,7 @@ export default class SVGLoader {
577
585
  const cleanUp = () => {
578
586
  svgElem.remove();
579
587
  sandbox.remove();
588
+ sandbox.src = '';
580
589
  };
581
590
  return { svgElem, cleanUp };
582
591
  }