From 2ec32aee559749e1978d779705c84a8343615bfe Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 22 Feb 2022 18:05:36 -0500
Subject: improve efficiency for scenes with lots of documents by using
 computed functions to avoid invalidations.  moved grid renderer to its own
 component to avoid invalidations when panning/zooming.

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 129 +++++++++++++--------
 1 file changed, 83 insertions(+), 46 deletions(-)

(limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')

diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 3443f33e1..3d664e146 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,5 +1,5 @@
 import { Bezier } from "bezier-js";
-import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction, trace } from "mobx";
 import { observer } from "mobx-react";
 import { computedFn } from "mobx-utils";
 import { DateField } from "../../../../fields/DateField";
@@ -95,6 +95,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     private _clusterDistance: number = 75;
     private _hitCluster: number = -1;
     private _disposers: { [name: string]: IReactionDisposer } = {};
+    private _renderCutoffData = observable.map<string, boolean>();
     private _layoutPoolData = observable.map<string, PoolData>();
     private _layoutSizeData = observable.map<string, { width?: number, height?: number }>();
     private _cachedPool: Map<string, PoolData> = new Map();
@@ -1155,7 +1156,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             renderDepth={this.props.renderDepth + 1}
             replica={entry.replica}
             renderIndex={renderIndex}
-            renderCutoff={this.NumLoaded}
+            renderCutoffProvider={this.renderCutoffProvider}
             ContainingCollectionView={this.props.CollectionView}
             ContainingCollectionDoc={this.props.Document}
             CollectionFreeFormView={this}
@@ -1259,6 +1260,12 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         }
     }
 
+
+    renderCutoffProvider = computedFn(function renderCutoffProvider(this: any, doc: Doc) {
+        return !this._renderCutoffData.get(doc[Id] + "");
+    }.bind(this));
+
+
     childPositionProviderUnmemoized = (doc: Doc, replica: string) => {
         return this._layoutPoolData.get(doc[Id] + (replica || ""));
     }
@@ -1304,7 +1311,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     }
 
     @observable _numLoaded = 1;
-    NumLoaded = () => this._numLoaded;
     get doLayoutComputation() {
         const { newPool, computedElementData } = this.doInternalLayoutComputation;
         const array = Array.from(newPool.entries());
@@ -1567,9 +1573,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
 
     incrementalRender = action(() => {
         if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) {
-            this._numLoaded++;
+            const unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id]));
+            const loadIncrement = 5;
+            for (var i = 0; i < Math.min(unrendered.length, loadIncrement); i++) {
+                this._renderCutoffData.set(unrendered[i][Id] + "", true);
+            }
         }
-        this._numLoaded < this.views.length && setTimeout(this.incrementalRender, 1);
+        this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1);
     });
 
     children = () => {
@@ -1582,46 +1592,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
         ];
     }
 
-    chooseGridSpace = (gridSpace: number): number => {
-        if (!this.zoomScaling()) return 50;
-        const divisions = this.props.PanelWidth() / this.zoomScaling() / gridSpace + 3;
-        return divisions < 60 ? gridSpace : this.chooseGridSpace(gridSpace * 10);
-    }
-
-    @computed get backgroundGrid() {
-        const gridSpace = this.chooseGridSpace(NumCast(this.layoutDoc["_backgroundGrid-spacing"], 50));
-        const shiftX = (this.props.isAnnotationOverlay ? 0 : -this.panX() % gridSpace - gridSpace) * this.zoomScaling();
-        const shiftY = (this.props.isAnnotationOverlay ? 0 : -this.panY() % gridSpace - gridSpace) * this.zoomScaling();
-        const renderGridSpace = gridSpace * this.zoomScaling();
-        const w = this.props.PanelWidth() + 2 * renderGridSpace;
-        const h = this.props.PanelHeight() + 2 * renderGridSpace;
-        const strokeStyle = CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark ? "rgba(255,255,255,0.5)" : "rgba(0, 0,0,0.5)";
-        return <canvas className="collectionFreeFormView-grid" width={w} height={h} style={{ transform: `translate(${shiftX}px, ${shiftY}px)` }}
-            ref={(el) => {
-                const ctx = el?.getContext('2d');
-                if (ctx) {
-                    const Cx = this.cachedCenteringShiftX % renderGridSpace;
-                    const Cy = this.cachedCenteringShiftY % renderGridSpace;
-                    ctx.lineWidth = Math.min(1, Math.max(0.5, this.zoomScaling()));
-                    ctx.setLineDash(gridSpace > 50 ? [3, 3] : [1, 5]);
-                    ctx.clearRect(0, 0, w, h);
-                    if (ctx) {
-                        ctx.strokeStyle = strokeStyle;
-                        ctx.beginPath();
-                        for (let x = Cx - renderGridSpace; x <= w - Cx; x += renderGridSpace) {
-                            ctx.moveTo(x, Cy - h);
-                            ctx.lineTo(x, Cy + h);
-                        }
-                        for (let y = Cy - renderGridSpace; y <= h - Cy; y += renderGridSpace) {
-                            ctx.moveTo(Cx - w, y);
-                            ctx.lineTo(Cx + w, y);
-                        }
-                        ctx.stroke();
-                    }
-                }
-            }} />;
-    }
-
     @computed get placeholder() {
         return <div className="collectionfreeformview-placeholder" style={{ background: this.Document.backgroundColor }}>
             <span className="collectionfreeformview-placeholderSpan">{this.props.Document.title?.toString()}</span>
@@ -1629,6 +1599,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
     }
 
     @computed get marqueeView() {
+        TraceMobx();
         return <MarqueeView
             {...this.props}
             ungroup={this.props.Document._isGroup ? this.promoteCollection : undefined}
@@ -1643,7 +1614,18 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
             getTransform={this.getTransform}
             isAnnotationOverlay={this.isAnnotationOverlay}>
             <div className="marqueeView-div" ref={this._marqueeRef} style={{ opacity: this.props.dontRenderDocuments ? 0 : undefined }}>
-                {this.layoutDoc._backgroundGridShow ? this.backgroundGrid : (null)}
+                {this.layoutDoc._backgroundGridShow ?
+                    <CollectionFreeFormBackgroundGrid
+                        PanelWidth={this.props.PanelWidth}
+                        PanelHeight={this.props.PanelHeight}
+                        panX={this.panX}
+                        panY={this.panY}
+                        zoomScaling={this.zoomScaling}
+                        layoutDoc={this.layoutDoc}
+                        isAnnotationOverlay={this.isAnnotationOverlay}
+                        cachedCenteringShiftX={this.cachedCenteringShiftX}
+                        cachedCenteringShiftY={this.cachedCenteringShiftY}
+                    /> : (null)}
                 <CollectionFreeFormViewPannableContents
                     isAnnotationOverlay={this.isAnnotationOverlay}
                     isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable}
@@ -1874,4 +1856,59 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF
             {this.zoomProgressivize}
         </div>;
     }
+}
+
+interface CollectionFreeFormViewBackgroundGridProps {
+    panX: () => number;
+    panY: () => number;
+    PanelWidth: () => number;
+    PanelHeight: () => number;
+    isAnnotationOverlay?: boolean;
+    zoomScaling: () => number;
+    layoutDoc: Doc;
+    cachedCenteringShiftX: number;
+    cachedCenteringShiftY: number;
+}
+@observer
+class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFormViewBackgroundGridProps> {
+
+
+    chooseGridSpace = (gridSpace: number): number => {
+        if (!this.props.zoomScaling()) return 50;
+        const divisions = this.props.PanelWidth() / this.props.zoomScaling() / gridSpace + 3;
+        return divisions < 60 ? gridSpace : this.chooseGridSpace(gridSpace * 10);
+    }
+    render() {
+        const gridSpace = this.chooseGridSpace(NumCast(this.props.layoutDoc["_backgroundGrid-spacing"], 50));
+        const shiftX = (this.props.isAnnotationOverlay ? 0 : -this.props.panX() % gridSpace - gridSpace) * this.props.zoomScaling();
+        const shiftY = (this.props.isAnnotationOverlay ? 0 : -this.props.panY() % gridSpace - gridSpace) * this.props.zoomScaling();
+        const renderGridSpace = gridSpace * this.props.zoomScaling();
+        const w = this.props.PanelWidth() + 2 * renderGridSpace;
+        const h = this.props.PanelHeight() + 2 * renderGridSpace;
+        const strokeStyle = CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark ? "rgba(255,255,255,0.5)" : "rgba(0, 0,0,0.5)";
+        return <canvas className="collectionFreeFormView-grid" width={w} height={h} style={{ transform: `translate(${shiftX}px, ${shiftY}px)` }}
+            ref={(el) => {
+                const ctx = el?.getContext('2d');
+                if (ctx) {
+                    const Cx = this.props.cachedCenteringShiftX % renderGridSpace;
+                    const Cy = this.props.cachedCenteringShiftY % renderGridSpace;
+                    ctx.lineWidth = Math.min(1, Math.max(0.5, this.props.zoomScaling()));
+                    ctx.setLineDash(gridSpace > 50 ? [3, 3] : [1, 5]);
+                    ctx.clearRect(0, 0, w, h);
+                    if (ctx) {
+                        ctx.strokeStyle = strokeStyle;
+                        ctx.beginPath();
+                        for (let x = Cx - renderGridSpace; x <= w - Cx; x += renderGridSpace) {
+                            ctx.moveTo(x, Cy - h);
+                            ctx.lineTo(x, Cy + h);
+                        }
+                        for (let y = Cy - renderGridSpace; y <= h - Cy; y += renderGridSpace) {
+                            ctx.moveTo(Cx - w, y);
+                            ctx.lineTo(Cx + w, y);
+                        }
+                        ctx.stroke();
+                    }
+                }
+            }} />;
+    }
 }
\ No newline at end of file
-- 
cgit v1.2.3-70-g09d2

