From b33ff1abdeab12d977c86f18c567ba3e84a49297 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Thu, 22 Feb 2024 16:34:32 -0500
Subject: replaced freeform document view wrapper with more direct caching of
 props.

---
 .../views/nodes/CollectionFreeFormDocumentView.tsx | 175 ++++++++-------------
 1 file changed, 64 insertions(+), 111 deletions(-)

(limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx')

diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index a0a64ab59..2800ea200 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,4 +1,4 @@
-import { action, computed, makeObservable, observable } from 'mobx';
+import { action, makeObservable, observable } from 'mobx';
 import { observer } from 'mobx-react';
 import * as React from 'react';
 import { OmitKeys, numberRange } from '../../../Utils';
@@ -12,14 +12,16 @@ import { DocumentManager } from '../../util/DocumentManager';
 import { ScriptingGlobals } from '../../util/ScriptingGlobals';
 import { SelectionManager } from '../../util/SelectionManager';
 import { DocComponent } from '../DocComponent';
-import { ObservableReactComponent } from '../ObservableReactComponent';
 import { StyleProp } from '../StyleProvider';
 import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
 import './CollectionFreeFormDocumentView.scss';
 import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
 import { FieldViewProps } from './FieldView';
 
-export interface CollectionFreeFormDocumentViewWrapperProps extends DocumentViewProps {
+/// Ugh, typescript has no run-time way of iterating through the keys of an interface.  so we need
+/// manaully keep this list of keys in synch wih the fields of the freeFormProps interface
+const freeFormPropsKeys = ['x', 'y', 'z', 'zIndex', 'rotation', 'opacity', 'backgroundColor', 'color', 'highlight', 'width', 'height', 'autoDim', 'transition'];
+interface freeFormProps {
     x: number;
     y: number;
     z: number;
@@ -33,102 +35,17 @@ export interface CollectionFreeFormDocumentViewWrapperProps extends DocumentView
     opacity?: number;
     highlight?: boolean;
     transition?: string;
-    RenderCutoffProvider: (doc: Doc) => boolean;
-    CollectionFreeFormView: CollectionFreeFormView;
-}
-@observer
-export class CollectionFreeFormDocumentViewWrapper extends ObservableReactComponent<CollectionFreeFormDocumentViewWrapperProps> {
-    constructor(props: CollectionFreeFormDocumentViewWrapperProps) {
-        super(props);
-        makeObservable(this);
-    }
-    @observable X = this.props.x;
-    @observable Y = this.props.y;
-    @observable Z = this.props.z;
-    @observable ZIndex = this.props.zIndex;
-    @observable Rotation = this.props.rotation;
-    @observable Opacity = this.props.opacity;
-    @observable BackgroundColor = this.props.backgroundColor;
-    @observable Color = this.props.color;
-    @observable Highlight = this.props.highlight;
-    @observable Width = this.props.width;
-    @observable Height = this.props.height;
-    @observable AutoDim = this.props.autoDim;
-    @observable Transition = this.props.transition;
-    CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking
-    RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking
-
-    get Document() {
-        return this._props.Document;
-    }
-    @computed get WrapperKeys() {
-        return Object.keys(this).filter(key => key.startsWith('w_')).map(key => key.replace('w_', ''))
-                  .map(key => ({upper:key, lower:key[0].toLowerCase() + key.substring(1)})); // prettier-ignore
-    }
-
-    // wrapper functions around prop fields that have been converted to observables to keep 'props' from ever changing.
-    // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes
-    w_X = ()               => this.X; // prettier-ignore
-    w_Y = ()               => this.Y; // prettier-ignore
-    w_Z = ()               => this.Z; // prettier-ignore
-    w_ZIndex = ()          => this.ZIndex ?? NumCast(this.Document.zIndex); // prettier-ignore
-    w_Rotation = ()        => this.Rotation ?? NumCast(this.Document._rotation); // prettier-ignore
-    w_Opacity = ()         => this.Opacity; // prettier-ignore
-    w_BackgroundColor = () => this.BackgroundColor ?? Cast(this.Document._backgroundColor, 'string', null); // prettier-ignore
-    w_Color = ()           => this.Color ?? Cast(this.Document._color, 'string', null); // prettier-ignore
-    w_Highlight = ()       => this.Highlight; // prettier-ignore
-    w_Width = ()           => this.Width; // prettier-ignore
-    w_Height = ()          => this.Height; // prettier-ignore
-    w_AutoDim = ()         => this.AutoDim; // prettier-ignore
-    w_Transition = ()      => this.Transition; // prettier-ignore
-
-    PanelWidth = ()        => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore
-    PanelHeight = ()       => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore
-
-    componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewWrapperProps & { fieldKey: string }>>) {
-        super.componentDidUpdate(prevProps);
-        this.WrapperKeys.forEach(action(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower])));
-    }
-    render() {
-        const layoutProps = this.WrapperKeys.reduce((val, keys) => [(val['w_' + keys.upper] = (this as any)['w_' + keys.upper]), val][1], {} as { [key: string]: Function });
-        return (
-            <CollectionFreeFormDocumentView
-                {...OmitKeys(this._props, this.WrapperKeys.map(keys => keys.lower) ).omit} // prettier-ignore
-                {...layoutProps}
-                PanelWidth={this.PanelWidth}
-                PanelHeight={this.PanelHeight}
-            />
-        );
-    }
 }
 export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
-    w_X: () => number;
-    w_Y: () => number;
-    w_Z: () => number;
-    w_ZIndex?: () => number;
-    w_Rotation?: () => number;
-    w_Color: () => string;
-    w_BackgroundColor: () => string;
-    w_Opacity: () => number | undefined;
-    w_Highlight: () => boolean | undefined;
-    w_Transition: () => string | undefined;
-    w_Width: () => number;
-    w_Height: () => number;
-    PanelWidth: () => number;
-    PanelHeight: () => number;
     RenderCutoffProvider: (doc: Doc) => boolean;
     CollectionFreeFormView: CollectionFreeFormView;
 }
-
 @observer
-export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps>() {
-    constructor(props: CollectionFreeFormDocumentViewProps) {
-        super(props);
-        makeObservable(this);
-    }
+export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps & freeFormProps>() {
     get displayName() { // this makes mobx trace() statements more descriptive
         return 'CollectionFreeFormDocumentView(' + this.Document.title + ')';
     } // prettier-ignore
+    public static CollectionFreeFormDocViewClassName = 'collectionFreeFormDocumentView-container';
     public static animFields: { key: string; val?: number }[] = [
         { key: 'x' },
         { key: 'y' },
@@ -145,16 +62,53 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
     public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames
     public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames
 
-    get CollectionFreeFormView() {
-        return this._props.CollectionFreeFormView;
+    constructor(props: CollectionFreeFormDocumentViewProps & freeFormProps) {
+        super(props);
+        makeObservable(this);
+    }
+
+    get WrapperKeys() {
+        // each of these keys is set by the CollectionView and passed via props. however, if any one of these props changes
+        // (or any other prop), then it's as if they all change.
+        // Anything that accesses these props will then invalidate unncessarily.
+        // To avoid this, we copy these prop values into local observables.  Now when 'props' changes, nothing invalidates.
+        // Instead, we copy each values into its observable which ohnly triggers invalidations if the new value is different
+        // than the old value, and then only things that access that observable will invalidate.
+        return freeFormPropsKeys
+                  .map(key => ({upper:key[0].toUpperCase() + key.substring(1), lower:key})); // prettier-ignore
+    }
+    @observable X = this.props.x;
+    @observable Y = this.props.y;
+    @observable Z = this.props.z;
+    @observable ZIndex = this.props.zIndex;
+    @observable Rotation = this.props.rotation;
+    @observable Opacity = this.props.opacity;
+    @observable BackgroundColor = this.props.backgroundColor;
+    @observable Color = this.props.color;
+    @observable Highlight = this.props.highlight;
+    @observable Width = this.props.width;
+    @observable Height = this.props.height;
+    @observable AutoDim = this.props.autoDim;
+    @observable Transition = this.props.transition;
+
+    componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewProps & freeFormProps>>) {
+        super.componentDidUpdate(prevProps);
+        this.WrapperKeys.forEach(action(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower])));
     }
 
+    CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking
+    // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes
+    DataTransition = ()  => this._props.transition; // prettier-ignore
+    RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking
+    PanelWidth = ()        => this._props.autoDim ? this._props.PanelWidth?.() : this.Width; // prettier-ignore
+    PanelHeight = ()       => this._props.autoDim ? this._props.PanelHeight?.() : this.Height; // prettier-ignore
+
     styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => {
         if (doc === this.layoutDoc) {
             switch (property) {
-                case StyleProp.Opacity:         return this._props.w_Opacity(); // only change the opacity for this specific document, not its children
-                case StyleProp.BackgroundColor: return this._props.w_BackgroundColor();
-                case StyleProp.Color:           return this._props.w_Color();
+                case StyleProp.Opacity:         return this.Opacity; // only change the opacity for this specific document, not its children
+                case StyleProp.BackgroundColor: return this.BackgroundColor;
+                case StyleProp.Color:           return this.Color;
             } // prettier-ignore
         }
         return this._props.styleProvider?.(doc, props, property);
@@ -253,14 +207,14 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
 
     nudge = (x: number, y: number) => {
         const [locX, locY] = this._props.ScreenToLocalTransform().transformDirection(x, y);
-        this.Document.x = this._props.w_X() + locX;
-        this.Document.y = this._props.w_Y() + locY;
+        this.Document.x = this.X + locX;
+        this.Document.y = this.Y + locY;
     };
     screenToLocalTransform = () =>
         this._props
             .ScreenToLocalTransform()
-            .translate(-this._props.w_X(), -this._props.w_Y())
-            .rotateDeg(-(this._props.w_Rotation?.() || 0));
+            .translate(-this.X, -this.Y)
+            .rotateDeg(-(this.Rotation || 0));
     returnThis = () => this;
 
     /// this indicates whether the doc view is activated because of its relationshop to a group
@@ -273,27 +227,26 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
         const isGroup = this.dataDoc.isGroup && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent');
         return isGroup ? (this._props.isDocumentActive?.() ? 'group' : this._props.isGroupActive?.() ? 'child' : 'inactive') : this._props.isGroupActive?.() ? 'child' : undefined;
     };
-    public static CollectionFreeFormDocViewClassName = 'collectionFreeFormDocumentView-container';
     render() {
         TraceMobx();
-        const passOnProps = OmitKeys(this._props, Object.keys(this._props).filter(key => key.startsWith('w_'))).omit; // prettier-ignore
+
         return (
             <div
                 className={CollectionFreeFormDocumentView.CollectionFreeFormDocViewClassName}
                 style={{
-                    width: this._props.PanelWidth(),
-                    height: this._props.PanelHeight(),
-                    transform: `translate(${this._props.w_X()}px, ${this._props.w_Y()}px) rotate(${NumCast(this._props.w_Rotation?.())}deg)`,
-                    transition: this._props.w_Transition?.() || StrCast(this.Document.dataTransition),
-                    zIndex: this._props.w_ZIndex?.(),
-                    display: this._props.w_Width?.() ? undefined : 'none',
+                    width: this.PanelWidth(),
+                    height: this.PanelHeight(),
+                    transform: `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Rotation)}deg)`,
+                    transition: this.Transition || StrCast(this.Document.dataTransition),
+                    zIndex: this.ZIndex,
+                    display: this.Width ? undefined : 'none',
                 }}>
-                {this._props.RenderCutoffProvider(this.Document) ? (
-                    <div style={{ position: 'absolute', width: this._props.PanelWidth(), height: this._props.PanelHeight(), background: 'lightGreen' }} />
+                {this.RenderCutoffProvider(this.Document) ? (
+                    <div style={{ position: 'absolute', width: this.PanelWidth(), height: this.PanelHeight(), background: 'lightGreen' }} />
                 ) : (
                     <DocumentView
-                        {...passOnProps}
-                        DataTransition={this._props.w_Transition}
+                        {...OmitKeys(this._props,this.WrapperKeys.map(val => val.lower)).omit} // prettier-ignore
+                        DataTransition={this.DataTransition}
                         CollectionFreeFormDocumentView={this.returnThis}
                         styleProvider={this.styleProvider}
                         ScreenToLocalTransform={this.screenToLocalTransform}
-- 
cgit v1.2.3-70-g09d2

