From daeae9db15372b8bd5f5aab387988051341c9209 Mon Sep 17 00:00:00 2001
From: bobzel <zzzman@gmail.com>
Date: Tue, 30 Apr 2024 13:28:15 -0400
Subject: broke out clusters code from freeformview to its own file.  cleaned
 up a bit.

---
 .../collectionFreeForm/CollectionFreeFormView.tsx  | 537 +++++----------------
 1 file changed, 132 insertions(+), 405 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 c1a889539..69cb52233 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -7,7 +7,7 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti
 import { observer } from 'mobx-react';
 import { computedFn } from 'mobx-utils';
 import * as React from 'react';
-import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents } from '../../../../ClientUtils';
+import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils';
 import { DateField } from '../../../../fields/DateField';
 import { Doc, DocListCast, Field, FieldType, Opt } from '../../../../fields/Doc';
 import { DocData, Height, Width } from '../../../../fields/DocSymbols';
@@ -48,19 +48,23 @@ import { DocumentView, OpenWhere } from '../../nodes/DocumentView';
 import { FieldViewProps, FocusViewOptions } from '../../nodes/FieldView';
 import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
 import { PresBox } from '../../nodes/trails/PresBox';
-// eslint-disable-next-line import/extensions
-import { CreateImage } from '../../nodes/WebBoxRenderer';
 import { StyleProp } from '../../StyleProvider';
 import { CollectionSubView } from '../CollectionSubView';
 import { TreeViewType } from '../CollectionTreeView';
 import { CollectionFreeFormBackgroundGrid } from './CollectionFreeFormBackgroundGrid';
 import { CollectionFreeFormInfoUI } from './CollectionFreeFormInfoUI';
+import { CollectionFreeFormClusters } from './CollectionFreeFormClusters';
 import { computePassLayout, computePivotLayout, computeStarburstLayout, computeTimelineLayout, PoolData, ViewDefBounds, ViewDefResult } from './CollectionFreeFormLayoutEngines';
 import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannableContents';
 import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
 import './CollectionFreeFormView.scss';
 import { MarqueeView } from './MarqueeView';
 
+class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
+    render() {
+        return this.props.elements().filter(ele => ele.bounds?.z).map(ele => ele.ele); // prettier-ignore
+    }
+}
 export interface collectionFreeformViewProps {
     NativeWidth?: () => number;
     NativeHeight?: () => number;
@@ -79,18 +83,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     public get displayName() {
         return 'CollectionFreeFormView(' + (this.Document.title?.toString() ?? '') + ')';
     } // this makes mobx trace() statements more descriptive
+    public unprocessedDocs: Doc[] = [];
+    public static collectionsWithUnprocessedInk = new Set<CollectionFreeFormView>();
 
-    @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, '');
-    @computed get paintFunc() {
-        const field = this.dataDoc[this.fieldKey];
-        const paintFunc = StrCast(Field.toJavascriptString(Cast(field, RichTextField, null)?.Text as FieldType)).trim();
-        return !paintFunc
-            ? ''
-            : paintFunc.includes('dashDiv')
-              ? `const dashDiv = document.querySelector('#${this._paintedId}');
-                    (async () => {  ${paintFunc} })()`
-              : paintFunc;
-    }
+    _oldWheel: any;
+    _clusters = new CollectionFreeFormClusters(this);
     constructor(props: any) {
         super(props);
         makeObservable(this);
@@ -102,41 +99,48 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     private _downX: number = 0;
     private _downY: number = 0;
     private _downTime = 0;
-    private _clusterDistance: number = 75;
-    private _hitCluster: number = -1;
     private _disposers: { [name: string]: IReactionDisposer } = {};
     private _renderCutoffData = observable.map<string, boolean>();
     private _batch: UndoManager.Batch | undefined = undefined;
     private _brushtimer: any;
     private _brushtimer1: any;
+    private _eraserLock = 0;
+    private _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed.  Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation.
 
-    public get isAnnotationOverlay() {
-        return this._props.isAnnotationOverlay;
-    }
-    public get scaleFieldKey() {
-        return (this._props.viewField ?? '') + '_freeform_scale';
-    }
-    private get panXFieldKey() {
-        return (this._props.viewField ?? '') + '_freeform_panX';
-    }
-    private get panYFieldKey() {
-        return (this._props.viewField ?? '') + '_freeform_panY';
-    }
-    private get autoResetFieldKey() {
-        return (this._props.viewField ?? '') + '_freeform_autoReset';
-    }
+    private get isAnnotationOverlay() { return this._props.isAnnotationOverlay; } // prettier-ignore
+    private get scaleFieldKey()       { return (this._props.viewField ?? '') + '_freeform_scale'; } // prettier-ignore
+    private get panXFieldKey()        { return (this._props.viewField ?? '') + '_freeform_panX'; } // prettier-ignore
+    private get panYFieldKey()        { return (this._props.viewField ?? '') + '_freeform_panY'; } // prettier-ignore
+    private get autoResetFieldKey()   { return (this._props.viewField ?? '') + '_freeform_autoReset'; } // prettier-ignore
 
     @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables
     @observable _panZoomTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan.  set to 0 to use document's transition time or default of 0
     @observable _firstRender = false; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown.  could be used for performance improvement
     @observable _showAnimTimeline = false;
-    @observable _clusterSets: Doc[][] = [];
     @observable _deleteList: DocumentView[] = [];
     @observable _timelineRef = React.createRef<Timeline>();
     @observable _marqueeViewRef = React.createRef<MarqueeView>();
     @observable _brushedView: { width: number; height: number; panX: number; panY: number } | undefined = undefined; // highlighted region of freeform canvas used by presentations to indicate a region
     @observable GroupChildDrag: boolean = false; // child document view being dragged.  needed to update drop areas of groups when a group item is dragged.
+    @observable _childPointerEvents: 'none' | 'all' | 'visiblepainted' | undefined = undefined;
+    @observable _lightboxDoc: Opt<Doc> = undefined;
+    @observable _paintedId = 'id' + Utils.GenerateGuid().replace(/-/g, '');
+    @observable _keyframeEditing = false;
 
+    @computed get layoutEngine() {
+        return this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine);
+    }
+    @computed get childPointerEvents() {
+        const engine = this._props.layoutEngine?.() || StrCast(this.Document._layoutEngine);
+        return SnappingManager.IsResizing
+            ? 'none'
+            : this._props.childPointerEvents?.() ??
+                  (this._props.viewDefDivClick || //
+                  (engine === computePassLayout.name && !this._props.isSelected()) ||
+                  this.isContentActive() === false
+                      ? 'none'
+                      : this._props.pointerEvents?.());
+    }
     @computed get contentViews() {
         const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1 && ele.inkMask !== undefined).map(ele => ele.ele);
         const renderableEles = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && (ele.inkMask === -1 || ele.inkMask === undefined)).map(ele => ele.ele);
@@ -158,14 +162,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return (this._props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox) && !this.isAnnotationOverlay;
     }
     @computed get contentBounds() {
-        const cb = Cast(this.dataDoc.contentBounds, listSpec('number'));
-        return cb
-            ? { x: cb[0], y: cb[1], r: cb[2], b: cb[3] }
-            : aggregateBounds(
-                  this._layoutElements.filter(e => e.bounds?.width && !e.bounds.z).map(e => e.bounds!),
-                  NumCast(this.layoutDoc._xPadding, this._props.xPadding ?? 10),
-                  NumCast(this.layoutDoc._yPadding, this._props.yPadding ?? 10)
-              );
+        return aggregateBounds(
+            this._layoutElements.filter(e => e.bounds?.width && !e.bounds.z).map(e => e.bounds!),
+            NumCast(this.layoutDoc._xPadding, this._props.xPadding ?? 10),
+            NumCast(this.layoutDoc._yPadding, this._props.yPadding ?? 10)
+        );
     }
     @computed get nativeWidth() {
         return this._props.NativeWidth?.() || Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null));
@@ -196,6 +197,27 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             .translate(-this.centeringShiftX, -this.centeringShiftY)
             .transform(this.panZoomXf);
     }
+    @computed get backgroundColor() {
+        return this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor);
+    }
+    @computed get nativeDimScaling() {
+        if (this._firstRender || (this._props.isAnnotationOverlay && !this._props.annotationLayerHostsContent)) return 1;
+        const nw = this.nativeWidth;
+        const nh = this.nativeHeight;
+        const hscale = nh ? this._props.PanelHeight() / nh : 1;
+        const wscale = nw ? this._props.PanelWidth() / nw : 1;
+        return wscale < hscale || (this._props.layout_fitWidth?.(this.Document) ?? this.layoutDoc.layout_fitWidth) ? wscale : hscale;
+    }
+    @computed get paintFunc() {
+        const field = this.dataDoc[this.fieldKey];
+        const paintFunc = StrCast(Field.toJavascriptString(Cast(field, RichTextField, null)?.Text as FieldType)).trim();
+        return !paintFunc
+            ? ''
+            : paintFunc.includes('dashDiv')
+              ? `const dashDiv = document.querySelector('#${this._paintedId}');
+                    (async () => {  ${paintFunc} })()`
+              : paintFunc;
+    }
 
     public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration: number) {
         return DocumentView.SetViewTransition(docs, 'all', duration, timer, undefined, true);
@@ -219,8 +241,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         });
         return newTimer;
     }
-
-    _keyTimer: NodeJS.Timeout | undefined; // timer for turning off transition flag when key frame change has completed.  Need to clear this if you do a second navigation before first finishes, or else first timer can go off during second naviation.
     changeKeyFrame = (back = false) => {
         const currentFrame = Cast(this.Document._currentFrame, 'number', null);
         if (currentFrame === undefined) {
@@ -236,11 +256,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             this.Document.lastFrame = Math.max(NumCast(this.Document._currentFrame), NumCast(this.Document.lastFrame));
         }
     };
-    @observable _keyframeEditing = false;
     @action setKeyFrameEditing = (set: boolean) => {
         this._keyframeEditing = set;
     };
     getKeyFrameEditing = () => this._keyframeEditing;
+
     onChildClickHandler = () => this._props.childClickScript || ScriptCast(this.Document.onChildClick);
     onChildDoubleClickHandler = () => this._props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick);
     elementFunc = () => this._layoutElements;
@@ -276,7 +296,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             retVal = this._props.addDocument?.(newBox) || false;
             if (retVal) {
                 this.bringToFront(newBox);
-                this.updateCluster(newBox);
+                this._clusters.updateCluster(newBox);
             }
         } else {
             retVal = this._props.addDocument?.(newBox) || false;
@@ -309,6 +329,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return dispTime === -1 || curTime === -1 || (curTime - dispTime >= -1e-4 && curTime <= endTime);
     }
 
+    /**
+     * focuses on a specified point in the freeform coordinate space.  (alternative to focusing on a Document)
+     * @param options
+     * @returns how long a transition it will be to focus on the point, or undefined the doc is a group or something else already moved
+     */
     focusOnPoint = (options: FocusViewOptions) => {
         const { pointFocus, zoomTime, didMove } = options;
         if (!this.Document.isGroup && pointFocus && !didMove) {
@@ -331,7 +356,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
      * @returns
      */
     groupFocus = (anchor: Doc, options: FocusViewOptions) => {
-        if (options.pointFocus) return this.focusOnPoint(options);
+        if (options.pointFocus) return undefined;
         options.docTransform = new Transform(NumCast(anchor.x) + NumCast(anchor._width)/2 - NumCast(this.layoutDoc[this.panXFieldKey]),
                                              NumCast(anchor.y) + NumCast(anchor._height)/2- NumCast(this.layoutDoc[this.panYFieldKey]), 1); // prettier-ignore
         const res = this._props.focus(this.Document, options);
@@ -339,6 +364,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return res;
     };
 
+    /**
+     * focuses the freeform view on the anchor subject to options.
+     *    If a pointFocus is specified, then groupFocus is triggered instad
+     *    Otherwise, this shifts the pan and zoom to the anchor target (as specified by options).
+     *    NOTE: focusing on a group only has an effet if the options contextPath is empty.
+     * @param anchor
+     * @param options
+     * @returns
+     */
     focus = (anchor: Doc, options: FocusViewOptions): any => {
         if (anchor.isGroup && !options.docTransform && options.contextPath?.length) {
             // don't focus on group if there's a context path because we're about to focus on a group item
@@ -349,7 +383,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         if (options.pointFocus) return this.focusOnPoint(options);
         const anchorInCollection = DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor);
         const anchorInChildViews = this.childLayoutPairs.map(pair => pair.layout).includes(anchor);
-        if (anchor.type !== DocumentType.CONFIG && !anchorInCollection && !anchorInChildViews) {
+        if (!anchorInCollection && !anchorInChildViews) {
             return undefined;
         }
         const xfToCollection = options?.docTransform ?? Transform.Identity();
@@ -420,7 +454,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             !d._keepZWhenDragged && (d.zIndex = zsorted.length + 1 + i); // bringToFront
         }
 
-        (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this.updateClusterDocs(docDragData.droppedDocuments);
+        (docDragData.droppedDocuments.length === 1 || de.shiftKey) && this._clusters.updateClusterDocs(docDragData.droppedDocuments);
         return true;
     }
 
@@ -469,189 +503,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
 
     onExternalDrop = (e: React.DragEvent) => (([x, y]) => super.onExternalDrop(e, { x, y }))(this.screenToFreeformContentsXf.transformPoint(e.pageX, e.pageY));
 
-    static overlapping(doc1: Doc, doc2: Doc, clusterDistance: number) {
-        const doc2Layout = Doc.Layout(doc2);
-        const doc1Layout = Doc.Layout(doc1);
-        const x2 = NumCast(doc2.x) - clusterDistance;
-        const y2 = NumCast(doc2.y) - clusterDistance;
-        const w2 = NumCast(doc2Layout._width) + clusterDistance;
-        const h2 = NumCast(doc2Layout._height) + clusterDistance;
-        const x = NumCast(doc1.x) - clusterDistance;
-        const y = NumCast(doc1.y) - clusterDistance;
-        const w = NumCast(doc1Layout._width) + clusterDistance;
-        const h = NumCast(doc1Layout._height) + clusterDistance;
-        return doc1.z === doc2.z && intersectRect({ left: x, top: y, width: w, height: h }, { left: x2, top: y2, width: w2, height: h2 });
-    }
-    pickCluster(probe: number[]) {
-        return this.childLayoutPairs
-            .map(pair => pair.layout)
-            .reduce((cluster, cd) => {
-                const grouping = this.Document._freeform_useClusters ? NumCast(cd.layout_cluster, -1) : NumCast(cd.group, -1);
-                if (grouping !== -1) {
-                    const layoutDoc = Doc.Layout(cd);
-                    const cx = NumCast(cd.x) - this._clusterDistance / 2;
-                    const cy = NumCast(cd.y) - this._clusterDistance / 2;
-                    const cw = NumCast(layoutDoc._width) + this._clusterDistance;
-                    const ch = NumCast(layoutDoc._height) + this._clusterDistance;
-                    return !layoutDoc.z && intersectRect({ left: cx, top: cy, width: cw, height: ch }, { left: probe[0], top: probe[1], width: 1, height: 1 }) ? grouping : cluster;
-                }
-                return cluster;
-            }, -1);
-    }
-
-    tryDragCluster(e: PointerEvent, cluster: number) {
-        if (cluster !== -1) {
-            const ptsParent = e;
-            if (ptsParent) {
-                const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === cluster);
-                const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.DocumentView?.())!);
-                const { left, top } = clusterDocs[0].getBounds || { left: 0, top: 0 };
-                const de = new DragManager.DocumentDragData(eles, e.ctrlKey || e.altKey ? dropActionType.embed : undefined);
-                de.moveDocument = this._props.moveDocument;
-                de.offset = this.screenToFreeformContentsXf.transformDirection(ptsParent.clientX - left, ptsParent.clientY - top);
-                DragManager.StartDocumentDrag(
-                    clusterDocs.map(v => v.ContentDiv!),
-                    de,
-                    ptsParent.clientX,
-                    ptsParent.clientY,
-                    { hideSource: !de.dropAction }
-                );
-                return true;
-            }
-        }
-
-        return false;
-    }
-
-    @action
-    updateClusters(useClusters: boolean) {
-        this.Document._freeform_useClusters = useClusters;
-        this._clusterSets.length = 0;
-        this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c));
-    }
-
-    @action
-    updateClusterDocs(docs: Doc[]) {
-        const childLayouts = this.childLayoutPairs.map(pair => pair.layout);
-        if (this.Document._freeform_useClusters) {
-            const docFirst = docs[0];
-            docs.forEach(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)));
-            const preferredInd = NumCast(docFirst.layout_cluster);
-            docs.forEach(doc => {
-                doc.layout_cluster = -1;
-            });
-            docs.map(doc =>
-                this._clusterSets.map((set, i) =>
-                    set.forEach(member => {
-                        if (docFirst.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && CollectionFreeFormView.overlapping(doc, member, this._clusterDistance)) {
-                            docFirst.layout_cluster = i;
-                        }
-                    })
-                )
-            );
-            if (
-                docFirst.layout_cluster === -1 &&
-                preferredInd !== -1 &&
-                this._clusterSets.length > preferredInd &&
-                (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)
-            ) {
-                docFirst.layout_cluster = preferredInd;
-            }
-            this._clusterSets.forEach((set, i) => {
-                if (docFirst.layout_cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) {
-                    docFirst.layout_cluster = i;
-                }
-            });
-            if (docFirst.layout_cluster === -1) {
-                docs.forEach(doc => {
-                    doc.layout_cluster = this._clusterSets.length;
-                    this._clusterSets.push([doc]);
-                });
-            } else if (this._clusterSets.length) {
-                for (let i = this._clusterSets.length; i <= NumCast(docFirst.layout_cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]);
-                docs.forEach(doc => {
-                    this._clusterSets[(doc.layout_cluster = NumCast(docFirst.layout_cluster))].push(doc);
-                });
-            }
-            childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.layout_cluster === i) && this.updateCluster(child));
-        }
-    }
-
-    @action
-    updateCluster = (doc: Doc) => {
-        const childLayouts = this.childLayoutPairs.map(pair => pair.layout);
-        if (this.Document._freeform_useClusters) {
-            this._clusterSets.forEach(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1));
-            const preferredInd = NumCast(doc.layout_cluster);
-            doc.layout_cluster = -1;
-            this._clusterSets.forEach((set, i) =>
-                set.forEach(member => {
-                    if (doc.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && CollectionFreeFormView.overlapping(doc, member, this._clusterDistance)) {
-                        doc.layout_cluster = i;
-                    }
-                })
-            );
-            if (doc.layout_cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) {
-                doc.layout_cluster = preferredInd;
-            }
-            this._clusterSets.forEach((set, i) => {
-                if (doc.layout_cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) {
-                    doc.layout_cluster = i;
-                }
-            });
-            if (doc.layout_cluster === -1) {
-                doc.layout_cluster = this._clusterSets.length;
-                this._clusterSets.push([doc]);
-            } else if (this._clusterSets.length) {
-                for (let i = this._clusterSets.length; i <= doc.layout_cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]);
-                this._clusterSets[doc.layout_cluster ?? 0].push(doc);
-            }
-        }
-    };
-
-    clusterStyleProvider = (doc: Opt<Doc>, props: Opt<FieldViewProps>, property: string) => {
-        let styleProp = this._props.styleProvider?.(doc, props, property); // bcz: check 'props'  used to be renderDepth + 1
-        if (doc && this.childDocList?.includes(doc))
-            switch (property.split(':')[0]) {
-                case StyleProp.BackgroundColor:
-                    {
-                        const cluster = NumCast(doc?.layout_cluster);
-                        if (this.Document._freeform_useClusters && doc?.type !== DocumentType.IMG) {
-                            if (this._clusterSets.length <= cluster) {
-                                setTimeout(() => doc && this.updateCluster(doc));
-                            } else {
-                                // choose a cluster color from a palette
-                                const colors = ['#da42429e', '#31ea318c', 'rgba(197, 87, 20, 0.55)', '#4a7ae2c4', 'rgba(216, 9, 255, 0.5)', '#ff7601', '#1dffff', 'yellow', 'rgba(27, 130, 49, 0.55)', 'rgba(0, 0, 0, 0.268)'];
-                                styleProp = colors[cluster % colors.length];
-                                const set = this._clusterSets[cluster]?.filter(s => s.backgroundColor);
-                                // override the cluster color with an explicitly set color on a non-background document.  then override that with an explicitly set color on a background document
-                                set?.forEach(s => {
-                                    styleProp = StrCast(s.backgroundColor);
-                                });
-                            }
-                        }
-                    }
-                    break;
-                case StyleProp.FillColor:
-                    if (doc && this.Document._currentFrame !== undefined) {
-                        return CollectionFreeFormDocumentView.getStringValues(doc, NumCast(this.Document._currentFrame))?.fillColor;
-                    }
-                    break;
-                default:
-            }
-        return styleProp;
-    };
-
-    trySelectCluster = (addToSel: boolean) => {
-        if (addToSel && this._hitCluster !== -1) {
-            !addToSel && SelectionManager.DeselectAll();
-            const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === this._hitCluster);
-            this.selectDocuments(eles);
-            return true;
-        }
-        return false;
-    };
-
     @action
     onPointerDown = (e: React.PointerEvent): void => {
         this._downX = this._lastX = e.pageX;
@@ -672,8 +523,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                         break;
                     case InkTool.None:
                         if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
-                            this._hitCluster = this.pickCluster(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
-                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1, false);
+                            const hit = this._clusters.pickCluster(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
+                            setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, hit !== -1, false);
                         }
                         break;
                     default:
@@ -682,25 +533,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
     };
 
-    public unprocessedDocs: Doc[] = [];
-    public static collectionsWithUnprocessedInk = new Set<CollectionFreeFormView>();
     @undoBatch
     onGesture = (e: Event, ge: GestureUtils.GestureEvent) => {
         switch (ge.gesture) {
-            // case Gestures.Rectangle:
-            //     {
-            //         const strokes = this.getActiveDocuments()
-            //             .filter(doc => doc.type === DocumentType.INK)
-            //             .map(i => {
-            //                 const d = Cast(i.stroke, InkField);
-            //                 const x = NumCast(i.x) - Math.min(...(d?.inkData.map(pd => pd.X) ?? [0]));
-            //                 const y = NumCast(i.y) - Math.min(...(d?.inkData.map(pd => pd.Y) ?? [0]));
-            //                 return !d ? [] : d.inkData.map(pd => ({ X: x + pd.X, Y: y + pd.Y }));
-            //             });
-
-            //         CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => {});
-            //     }
-            //     break;
             case Gestures.Text:
                 if (ge.text) {
                     const B = this.screenToFreeformContentsXf.transformPoint(ge.points[0].X, ge.points[0].Y);
@@ -756,7 +591,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
     };
 
-    @action
     scrollPan = (e: WheelEvent | { deltaX: number; deltaY: number }): void => {
         PresBox.Instance?.pauseAutoPres();
         this.setPan(NumCast(this.Document[this.panXFieldKey]) - e.deltaX, NumCast(this.Document[this.panYFieldKey]) - e.deltaY, 0, true);
@@ -775,7 +609,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this._lastY = e.clientY;
     };
 
-    _eraserLock = 0;
     /**
      * Erases strokes by intersecting them with an invisible "eraser stroke".
      *  By default this iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments,
@@ -816,7 +649,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     onPointerMove = (e: PointerEvent) => {
-        if (this.tryDragCluster(e, this._hitCluster)) {
+        if (this._clusters.tryDragCluster(e)) {
             e.stopPropagation(); // we're moving a cluster, so stop propagation and return true to end panning and let the document drag take over
             return true;
         }
@@ -835,20 +668,18 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     getEraserIntersections = (lastPoint: { X: number; Y: number }, currPoint: { X: number; Y: number }) => {
         const eraserMin = { X: Math.min(lastPoint.X, currPoint.X), Y: Math.min(lastPoint.Y, currPoint.Y) };
         const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) };
-
-        return this.childDocs
+        // prettier-ignore
+        return this.childDocs 
             .map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.()))
             .filter(inkView => inkView?.ComponentView instanceof InkingStroke)
             .map(inkView => inkView!)
             .map(inkView => ({ inkViewBounds: inkView.getBounds, inkStroke: inkView.ComponentView as InkingStroke, inkView }))
-            .filter(
-                ({ inkViewBounds }) =>
+            .filter(({ inkViewBounds }) =>
                     inkViewBounds && // bounding box of eraser segment and ink stroke overlap
                     eraserMin.X <= inkViewBounds.right &&
                     eraserMin.Y <= inkViewBounds.bottom &&
                     eraserMax.X >= inkViewBounds.left &&
-                    eraserMax.Y >= inkViewBounds.top
-            )
+                    eraserMax.Y >= inkViewBounds.top) 
             .reduce(
                 (intersections, { inkStroke, inkView }) => {
                     const { inkData } = inkStroke.inkScaledData();
@@ -1231,19 +1062,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
         return undefined;
     };
-    @computed get childPointerEvents() {
-        const engine = this._props.layoutEngine?.() || StrCast(this.Document._layoutEngine);
-        return SnappingManager.IsResizing
-            ? 'none'
-            : this._props.childPointerEvents?.() ??
-                  (this._props.viewDefDivClick || //
-                  (engine === computePassLayout.name && !this._props.isSelected()) ||
-                  this.isContentActive() === false
-                      ? 'none'
-                      : this._props.pointerEvents?.());
-    }
-
-    @observable _childPointerEvents: 'none' | 'all' | 'visiblepainted' | undefined = undefined;
     childPointerEventsFunc = () => this._childPointerEvents;
     childContentsActive = () => (this._props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction)();
     getChildDocView(entry: PoolData) {
@@ -1256,7 +1074,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 key={childLayout[Id] + (entry.replica || '')}
                 Document={childLayout}
                 containerViewPath={this.DocumentView?.().docViewPath}
-                styleProvider={this.clusterStyleProvider}
+                styleProvider={this._clusters.styleProvider}
                 TemplateDataDocument={childData}
                 dragStarting={this.dragStarting}
                 dragEnding={this.dragEnding}
@@ -1333,7 +1151,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         }
         return this._props.addDocTab(docs, where);
     });
-    @observable _lightboxDoc: Opt<Doc> = undefined;
 
     getCalculatedPositions(pair: { layout: Doc; data?: Doc }): PoolData {
         const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min);
@@ -1351,16 +1168,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         const rotation = Cast(_rotation,'number',
             !this.layoutDoc._rotation_jitter ? null
                 : NumCast(this.layoutDoc._rotation_jitter) * random(-1, 1, NumCast(x), NumCast(y)) );
-        const childProps = { ...this._props, fieldKey: '', styleProvider: this.clusterStyleProvider };
+        const childProps = { ...this._props, fieldKey: '', styleProvider: this._clusters.styleProvider };
         return {
             x: isNaN(NumCast(x)) ? 0 : NumCast(x),
             y: isNaN(NumCast(y)) ? 0 : NumCast(y),
             z: Cast(z, 'number'),
             autoDim,
             rotation,
-            color: Cast(color, 'string') ? StrCast(color) : this.clusterStyleProvider(childDoc, childProps, StyleProp.Color),
-            backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.clusterStyleProvider(childDoc, childProps, StyleProp.BackgroundColor),
-            opacity: !_width ? 0 : this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this.clusterStyleProvider?.(childDoc, childProps, StyleProp.Opacity),
+            color: Cast(color, 'string') ? StrCast(color) : this._clusters.styleProvider(childDoc, childProps, StyleProp.Color),
+            backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this._clusters.styleProvider(childDoc, childProps, StyleProp.BackgroundColor),
+            opacity: !_width ? 0 : this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this._clusters.styleProvider?.(childDoc, childProps, StyleProp.Opacity),
             zIndex: Cast(zIndex, 'number'),
             width: _width,
             height: _height,
@@ -1436,9 +1253,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         return [] as ViewDefResult[];
     }
 
-    @computed get layoutEngine() {
-        return this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine);
-    }
     @computed get doInternalLayoutComputation() {
         TraceMobx();
         const newPool = new Map<string, PoolData>();
@@ -1451,7 +1265,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         } // prettier-ignore
     }
 
-    @action
     doLayoutComputation = (newPool: Map<string, PoolData>, computedElementData: ViewDefResult[]) => {
         const elements = computedElementData.slice();
         Array.from(newPool.entries())
@@ -1464,7 +1277,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 })
             );
 
-        this.Document._freeform_useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true);
+        this._clusters.initClusters();
         return elements;
     };
 
@@ -1489,7 +1302,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
     };
 
     childDocsFunc = () => this.childDocs;
-    @action closeInfo = () => { Doc.IsInfoUIDisabled = true }; // prettier-ignore
+    closeInfo = action(() => { Doc.IsInfoUIDisabled = true }); // prettier-ignore
     infoUI = () => (Doc.IsInfoUIDisabled || this.Document.annotationOn || this._props.renderDepth ? null : <CollectionFreeFormInfoUI Document={this.Document} LayoutDoc={this.layoutDoc} childDocs={this.childDocsFunc} close={this.closeInfo} />);
 
     componentDidMount() {
@@ -1566,40 +1379,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         );
     }
 
-    static replaceCanvases(oldDiv: HTMLElement, newDiv: HTMLElement) {
-        if (oldDiv.childNodes && newDiv) {
-            for (let i = 0; i < oldDiv.childNodes.length; i++) {
-                this.replaceCanvases(oldDiv.childNodes[i] as HTMLElement, newDiv.childNodes[i] as HTMLElement);
-            }
-        }
-        if (oldDiv instanceof HTMLCanvasElement) {
-            if (oldDiv.className === 'collectionFreeFormView-grid') {
-                const newCan = newDiv as HTMLCanvasElement;
-                const parEle = newCan.parentElement as HTMLElement;
-                parEle.removeChild(newCan);
-                parEle.appendChild(document.createElement('div'));
-            } else {
-                const canvas = oldDiv;
-                const img = document.createElement('img'); // create a Image Element
-                try {
-                    img.src = canvas.toDataURL(); // image source
-                } catch (e) {
-                    console.log(e);
-                }
-                img.style.width = canvas.style.width;
-                img.style.height = canvas.style.height;
-                const newCan = newDiv as HTMLCanvasElement;
-                if (newCan) {
-                    const parEle = newCan.parentElement as HTMLElement;
-                    parEle.removeChild(newCan);
-                    parEle.appendChild(img);
-                }
-            }
-        }
+    componentWillUnmount() {
+        this.dataDoc[this.autoResetFieldKey] && this.resetView();
+        Object.values(this._disposers).forEach(disposer => disposer?.());
     }
 
     updateIcon = () =>
-        CollectionFreeFormView.UpdateIcon(
+        UpdateIcon(
             this.layoutDoc[Id] + '-icon' + new Date().getTime(),
             this.DocumentView?.().ContentDiv!,
             NumCast(this.layoutDoc._width),
@@ -1617,40 +1403,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             }
         );
 
-    public static UpdateIcon(
-        filename: string,
-        docViewContent: HTMLElement,
-        width: number,
-        height: number,
-        panelWidth: number,
-        panelHeight: number,
-        scrollTop: number,
-        realNativeHeight: number,
-        noSuffix: boolean,
-        replaceRootFilename: string | undefined,
-        cb: (iconFile: string, nativeWidth: number, nativeHeight: number) => any
-    ) {
-        const newDiv = docViewContent.cloneNode(true) as HTMLDivElement;
-        newDiv.style.width = width.toString();
-        newDiv.style.height = height.toString();
-        this.replaceCanvases(docViewContent, newDiv);
-        const htmlString = new XMLSerializer().serializeToString(newDiv);
-        const nativeWidth = width;
-        const nativeHeight = height;
-        return CreateImage(ClientUtils.prepend(''), document.styleSheets, htmlString, nativeWidth, (nativeWidth * panelHeight) / panelWidth, (scrollTop * panelHeight) / realNativeHeight)
-            .then(async (dataUrl: any) => {
-                const returnedFilename = await ClientUtils.convertDataUri(dataUrl, filename, noSuffix, replaceRootFilename);
-                cb(returnedFilename as string, nativeWidth, nativeHeight);
-            })
-            .catch((error: any) => console.error('oops, something went wrong!', error));
-    }
-
-    componentWillUnmount() {
-        this.dataDoc[this.autoResetFieldKey] && this.resetView();
-        Object.values(this._disposers).forEach(disposer => disposer?.());
-    }
-
-    @action
     onCursorMove = () => {
         //  super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
     };
@@ -1734,7 +1486,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this.Document.isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' });
 
         !Doc.noviceMode ? appearanceItems.push({ description: 'Arrange contents in grid', event: this.layoutDocsInGrid, icon: 'table' }) : null;
-        !Doc.noviceMode ? appearanceItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : null;
+        !Doc.noviceMode ? appearanceItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this._clusters.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : null;
         !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' });
 
         const options = ContextMenu.Instance.findByDescription('Options...');
@@ -1827,21 +1579,32 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1);
     });
 
-    @computed get placeholder() {
-        return (
-            <div className="collectionfreeformview-placeholder" style={{ background: this._props.styleProvider?.(this.Document, this._props, StyleProp.BackgroundColor) }}>
-                <span className="collectionfreeformview-placeholderSpan">{this.Document.annotationOn ? '' : this.Document.title?.toString()}</span>
-            </div>
-        );
-    }
-
     showPresPaths = () => SnappingManager.ShowPresPaths;
     brushedView = () => this._brushedView;
-    gridColor = () =>
-        DashColor(lightOrDark(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor)))
-            .fade(0.5)
-            .toString();
-    @computed get backgroundGrid() {
+    gridColor = () => DashColor(lightOrDark(this.backgroundColor)).fade(0.5).toString(); // prettier-ignore
+    nativeDim = () => this.nativeDimScaling;
+
+    brushView = action((viewport: { width: number; height: number; panX: number; panY: number }, transTime: number, holdTime: number = 2500) => {
+        this._brushtimer1 && clearTimeout(this._brushtimer1);
+        this._brushtimer && clearTimeout(this._brushtimer);
+        this._brushedView = undefined;
+        this._brushtimer1 = setTimeout(
+            action(() => {
+                this._brushedView = { ...viewport, panX: viewport.panX - viewport.width / 2, panY: viewport.panY - viewport.height / 2 };
+                this._brushtimer = setTimeout(action(() => { this._brushedView = undefined; }), holdTime); // prettier-ignore
+            }),
+            transTime + 1
+        );
+    });
+    lightboxPanelWidth = () => Math.max(0, this._props.PanelWidth() - 30);
+    lightboxPanelHeight = () => Math.max(0, this._props.PanelHeight() - 30);
+    lightboxScreenToLocal = () => this.ScreenToLocalBoxXf().translate(-15, -15);
+    onPassiveWheel = (e: WheelEvent) => {
+        const docHeight = NumCast(this.Document[Doc.LayoutFieldKey(this.Document) + '_nativeHeight'], this.nativeHeight);
+        const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this._props.PanelHeight() / this.nativeDimScaling;
+        this._props.isSelected() && !scrollable && e.preventDefault();
+    };
+    get backgroundGrid() {
         return (
             <div>
                 <CollectionFreeFormBackgroundGrid // bcz : UGHH  don't know why, but if we don't wrap in a div, then PDF's don't render when taking snapshot of a dashboard and the background grid is on!!?
@@ -1886,7 +1649,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 nudge={this.isAnnotationOverlay || this._props.renderDepth > 0 ? undefined : this.nudge}
                 addDocTab={this.addDocTab}
                 slowLoadDocuments={this.slowLoadDocuments}
-                trySelectCluster={this.trySelectCluster}
+                trySelectCluster={this._clusters.trySelectCluster}
                 activeDocuments={this.getActiveDocuments}
                 selectDocuments={this.selectDocuments}
                 addDocument={this.addDocument}
@@ -1902,39 +1665,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
             </MarqueeView>
         );
     }
-
-    @computed get nativeDimScaling() {
-        if (this._firstRender || (this._props.isAnnotationOverlay && !this._props.annotationLayerHostsContent)) return 1;
-        const nw = this.nativeWidth;
-        const nh = this.nativeHeight;
-        const hscale = nh ? this._props.PanelHeight() / nh : 1;
-        const wscale = nw ? this._props.PanelWidth() / nw : 1;
-        return wscale < hscale || (this._props.layout_fitWidth?.(this.Document) ?? this.layoutDoc.layout_fitWidth) ? wscale : hscale;
-    }
-    nativeDim = () => this.nativeDimScaling;
-
-    @action
-    brushView = (viewport: { width: number; height: number; panX: number; panY: number }, transTime: number, holdTime: number = 2500) => {
-        this._brushtimer1 && clearTimeout(this._brushtimer1);
-        this._brushtimer && clearTimeout(this._brushtimer);
-        this._brushedView = undefined;
-        this._brushtimer1 = setTimeout(
-            action(() => {
-                this._brushedView = { ...viewport, panX: viewport.panX - viewport.width / 2, panY: viewport.panY - viewport.height / 2 };
-                this._brushtimer = setTimeout(action(() => { this._brushedView = undefined; }), holdTime); // prettier-ignore
-            }),
-            transTime + 1
+    get placeholder() {
+        return (
+            <div className="collectionfreeformview-placeholder" style={{ background: this.backgroundColor }}>
+                <span className="collectionfreeformview-placeholderSpan">{this.Document.annotationOn ? '' : this.Document.title?.toString()}</span>
+            </div>
         );
-    };
-    lightboxPanelWidth = () => Math.max(0, this._props.PanelWidth() - 30);
-    lightboxPanelHeight = () => Math.max(0, this._props.PanelHeight() - 30);
-    lightboxScreenToLocal = () => this.ScreenToLocalBoxXf().translate(-15, -15);
-    onPassiveWheel = (e: WheelEvent) => {
-        const docHeight = NumCast(this.Document[Doc.LayoutFieldKey(this.Document) + '_nativeHeight'], this.nativeHeight);
-        const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this._props.PanelHeight() / this.nativeDimScaling;
-        this._props.isSelected() && !scrollable && e.preventDefault();
-    };
-    _oldWheel: any;
+    }
     render() {
         TraceMobx();
         return (
@@ -1992,10 +1729,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
                 ) : (
                     <>
                         {this._firstRender ? this.placeholder : this.marqueeView}
-                        {
-                            // eslint-disable-next-line no-use-before-define
-                            this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />
-                        }
+                        {this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />}
                         {!this.GroupChildDrag ? null : <div className="collectionFreeForm-groupDropper" />}
                     </>
                 )}
@@ -2003,13 +1737,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
         );
     }
 }
-
-@observer
-class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
-    render() {
-        return this.props.elements().filter(ele => ele.bounds?.z).map(ele => ele.ele); // prettier-ignore
-    }
-}
 // eslint-disable-next-line prefer-arrow-callback
 ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) {
     !readOnly && (SelectionManager.Views[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame();
-- 
cgit v1.2.3-70-g09d2

