diff options
Diffstat (limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')
| -rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 215 | 
1 files changed, 159 insertions, 56 deletions
| diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 8af048d67..1033050b9 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -7,7 +7,7 @@ import { Id } from "../../../../fields/FieldSymbols";  import { InkData, InkField, InkTool } from "../../../../fields/InkField";  import { List } from "../../../../fields/List";  import { RichTextField } from "../../../../fields/RichTextField"; -import { createSchema, makeInterface } from "../../../../fields/Schema"; +import { createSchema, makeInterface, listSpec } from "../../../../fields/Schema";  import { ScriptField } from "../../../../fields/ScriptField";  import { BoolCast, Cast, FieldValue, NumCast, ScriptCast, StrCast } from "../../../../fields/Types";  import { TraceMobx } from "../../../../fields/util"; @@ -33,7 +33,7 @@ import { ContextMenu } from "../../ContextMenu";  import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkingStroke";  import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";  import { DocumentLinksButton } from "../../nodes/DocumentLinksButton"; -import { DocumentViewProps } from "../../nodes/DocumentView"; +import { DocumentViewProps, DocAfterFocusFunc } from "../../nodes/DocumentView";  import { FormattedTextBox } from "../../nodes/formattedText/FormattedTextBox";  import { pageSchema } from "../../nodes/ImageBox";  import { PresBox } from "../../nodes/PresBox"; @@ -46,6 +46,7 @@ import "./CollectionFreeFormView.scss";  import { MarqueeOptionsMenu } from "./MarqueeOptionsMenu";  import { MarqueeView } from "./MarqueeView";  import React = require("react"); +import { CurrentUserUtils } from "../../../util/CurrentUserUtils";  export const panZoomSchema = createSchema({      _panX: "number", @@ -88,6 +89,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P      private _clusterDistance: number = 75;      private _hitCluster = false;      private _layoutComputeReaction: IReactionDisposer | undefined; +    private _boundsReaction: IReactionDisposer | undefined;      private _layoutPoolData = new ObservableMap<string, PoolData>();      private _layoutSizeData = new ObservableMap<string, { width?: number, height?: number }>();      private _cachedPool: Map<string, PoolData> = new Map(); @@ -105,27 +107,31 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P      @computed get fitToContentScaling() { return this.fitToContent ? NumCast(this.layoutDoc.fitToContentScaling, 1) : 1; }      @computed get fitToContent() { return (this.props.fitToBox || this.Document._fitToBox) && !this.isAnnotationOverlay; } -    @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent && !this.isAnnotationOverlay ? this.props.ContentScaling() : 1; } +    @computed get parentScaling() { return this.props.ContentScaling && this.fitToContent ? this.props.ContentScaling() : 1; }      @computed get contentBounds() { return aggregateBounds(this._layoutElements.filter(e => e.bounds && !e.bounds.z).map(e => e.bounds!), NumCast(this.layoutDoc._xPadding, 10), NumCast(this.layoutDoc._yPadding, 10)); } -    @computed get nativeWidth() { return this.fitToContent ? 0 : returnVal(this.props.NativeWidth?.(), NumCast(this.Document._nativeWidth)); } -    @computed get nativeHeight() { return this.fitToContent ? 0 : returnVal(this.props.NativeHeight?.(), NumCast(this.Document._nativeHeight)); } +    @computed get nativeWidth() { return this.fitToContent ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.Document)); } +    @computed get nativeHeight() { return this.fitToContent ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.Document)); }      private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }      private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; }      private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; } -    private panX = () => this.fitToContent ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0; -    private panY = () => this.fitToContent ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0; -    private zoomScaling = () => (this.fitToContentScaling / this.parentScaling) * (this.fitToContent ? -        Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), -            this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)) : -        NumCast(this.Document[this.scaleFieldKey], 1)) - +    private panX = () => this.fitToContent && !this.props.isAnnotationOverlay ? (this.contentBounds.x + this.contentBounds.r) / 2 : this.Document._panX || 0; +    private panY = () => this.fitToContent && !this.props.isAnnotationOverlay ? (this.contentBounds.y + this.contentBounds.b) / 2 : this.Document._panY || 0; +    private zoomScaling = () => { +        const mult = this.fitToContentScaling / this.parentScaling; +        if (this.fitToContent) { +            const zs = !this.childDocs.length ? 1 : +                Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)); +            return mult * zs; +        } +        return mult * NumCast(this.Document[this.scaleFieldKey], 1); +    }      @computed get cachedCenteringShiftX(): number {          const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; -        return !this.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0;  // shift so pan position is at center of window for non-overlay collections +        return !this.props.isAnnotationOverlay ? this.props.PanelWidth() / 2 / this.parentScaling / scaling : 0;  // shift so pan position is at center of window for non-overlay collections      }      @computed get cachedCenteringShiftY(): number {          const scaling = this.fitToContent || !this.contentScaling ? 1 : this.contentScaling; -        return !this.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections +        return !this.props.isAnnotationOverlay ? this.props.PanelHeight() / 2 / this.parentScaling / scaling : 0;// shift so pan position is at center of window for non-overlay collections      }      @computed get cachedGetLocalTransform(): Transform {          return Transform.Identity().scale(1 / this.zoomScaling()).translate(this.panX(), this.panY()); @@ -163,14 +169,23 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P                  if (newBox.activeFrame !== undefined) {                      const x = newBox.x;                      const y = newBox.y; +                    const w = newBox._width; +                    const h = newBox._height;                      delete newBox["x-indexed"];                      delete newBox["y-indexed"]; +                    delete newBox["w-indexed"]; +                    delete newBox["h-indexed"];                      delete newBox["opacity-indexed"]; +                    delete newBox._width; +                    delete newBox._height;                      delete newBox.x;                      delete newBox.y; +                    delete newBox.opacity;                      delete newBox.activeFrame;                      newBox.x = x;                      newBox.y = y; +                    newBox._width = w; +                    newBox._height = h;                  }              }              if (this.Document._currentFrame !== undefined && !this.props.isAnnotationOverlay) { @@ -197,13 +212,15 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P      @action      internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number, yp: number) {          if (!super.onInternalDrop(e, de)) return false; +        const refDoc = docDragData.droppedDocuments[0];          const [xpo, ypo] = this.getTransformOverlay().transformPoint(de.x, de.y); -        const z = NumCast(docDragData.droppedDocuments[0].z); +        const z = NumCast(refDoc.z);          const x = (z ? xpo : xp) - docDragData.offset[0];          const y = (z ? ypo : yp) - docDragData.offset[1];          const zsorted = this.childLayoutPairs.map(pair => pair.layout).slice().sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex));          zsorted.forEach((doc, index) => doc.zIndex = doc.isInkMask ? 5000 : index + 1); -        const dropPos = [NumCast(docDragData.droppedDocuments[0].x), NumCast(docDragData.droppedDocuments[0].y)]; +        const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000)); +        const dropPos = this.Document._currentFrame !== undefined ? [dvals.x, dvals.y] : [NumCast(refDoc.x), NumCast(refDoc.y)];          for (let i = 0; i < docDragData.droppedDocuments.length; i++) {              const d = docDragData.droppedDocuments[i];              const layoutDoc = Doc.Layout(d); @@ -214,7 +231,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P                  d.x = x + NumCast(d.x) - dropPos[0];                  d.y = y + NumCast(d.y) - dropPos[1];              } -            const nd = [NumCast(layoutDoc._nativeWidth), NumCast(layoutDoc._nativeHeight)]; +            const nd = [Doc.NativeWidth(layoutDoc), Doc.NativeHeight(layoutDoc)];              layoutDoc._width = NumCast(layoutDoc._width, 300);              layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? nd[1] / nd[0] * NumCast(layoutDoc._width) : 300);              !d._isBackground && (d._raiseWhenDragged === undefined ? Doc.UserDoc()._raiseWhenDragged : d._raiseWhenDragged) && (d.zIndex = zsorted.length + 1 + i); // bringToFront @@ -776,15 +793,19 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P      @action      zoom = (pointX: number, pointY: number, deltaY: number): void => {          let deltaScale = deltaY > 0 ? (1 / 1.05) : 1.05; -        if (deltaScale * this.zoomScaling() < 1 && this.isAnnotationOverlay) { -            deltaScale = 1 / this.zoomScaling(); -        }          if (deltaScale < 0) deltaScale = -deltaScale;          const [x, y] = this.getTransform().transformPoint(pointX, pointY); +        const invTransform = this.getLocalTransform().inverse(); +        if (deltaScale * invTransform.Scale > 20) { +            deltaScale = 20 / invTransform.Scale; +        } +        if (deltaScale * invTransform.Scale < 1 && this.isAnnotationOverlay) { +            deltaScale = 1 / invTransform.Scale; +        }          const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y);          if (localTransform.Scale >= 0.15 || localTransform.Scale > this.zoomScaling()) { -            const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 40); +            const safeScale = Math.min(Math.max(0.15, localTransform.Scale), 20);              this.props.Document[this.scaleFieldKey] = Math.abs(safeScale);              this.setPan(-localTransform.TranslateX / safeScale, -localTransform.TranslateY / safeScale);          } @@ -792,7 +813,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P      @action      onPointerWheel = (e: React.WheelEvent): void => { -        if (this.layoutDoc._lockedTransform || this.props.Document.inOverlay || this.props.Document.treeViewOutlineMode) return; +        if (this.layoutDoc._lockedTransform || CurrentUserUtils.OverlayDocs.includes(this.props.Document) || this.props.Document.treeViewOutlineMode) return;          if (!e.ctrlKey && this.props.Document.scrollHeight !== undefined) { // things that can scroll vertically should do that instead of zooming              e.stopPropagation();          } @@ -828,7 +849,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P                  else if (ranges.yrange.max <= (panY - panelDim[1] / 2)) panY = ranges.yrange.min - panelDim[1] / 2;              }          } -        if (!this.layoutDoc._lockedTransform || this.Document.inOverlay) { +        if (!this.layoutDoc._lockedTransform || CurrentUserUtils.OverlayDocs.includes(this.Document)) {              this.Document._viewTransition = panType;              const scale = this.getLocalTransform().inverse().Scale;              const newPanX = Math.min((1 - 1 / scale) * this.nativeWidth, Math.max(0, panX)); @@ -866,7 +887,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P          this.layoutDoc._panY = NumCast(this.layoutDoc._panY) - newpan[1];      } -    focusDocument = (doc: Doc, willZoom: boolean, scale?: number, afterFocus?: () => boolean) => { +    focusDocument = (doc: Doc, willZoom?: boolean, scale?: number, afterFocus?: DocAfterFocusFunc, dontCenter?: boolean, didFocus?: boolean) => {          const state = HistoryUtil.getState();          // TODO This technically isn't correct if type !== "doc", as @@ -885,48 +906,72 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P          SelectionManager.DeselectAll();          if (this.props.Document.scrollHeight) {              const annotOn = Cast(doc.annotationOn, Doc) as Doc; +            let delay = 1000;              if (!annotOn) { -                this.props.focus(doc); +                !dontCenter && this.props.focus(doc); +                afterFocus && setTimeout(afterFocus, delay);              } else {                  const contextHgt = Doc.AreProtosEqual(annotOn, this.props.Document) && this.props.VisibleHeight ? this.props.VisibleHeight() : NumCast(annotOn._height); -                const offset = annotOn && (contextHgt / 2); -                this.props.Document._scrollY = NumCast(doc.y) - offset; +                const curScroll = NumCast(this.props.Document._scrollTop); +                let scrollTo = curScroll; +                if (curScroll + contextHgt < NumCast(doc.y)) { +                    scrollTo = NumCast(doc.y) + Math.max(NumCast(doc._height), 100) - contextHgt; +                } else if (curScroll > NumCast(doc.y)) { +                    scrollTo = Math.max(0, NumCast(doc.y) - 50); +                } +                if (curScroll !== scrollTo || this.props.Document._viewTransition) { +                    this.props.Document._scrollPreviewY = this.props.Document._scrollY = scrollTo; +                    delay = Math.abs(scrollTo - curScroll) > 5 ? 1000 : 0; +                    !dontCenter && this.props.focus(this.props.Document); +                    afterFocus && setTimeout(afterFocus, delay); +                } else { +                    !dontCenter && delay && this.props.focus(this.props.Document); +                    afterFocus?.(!dontCenter && delay ? true : false); + +                }              } -            afterFocus && setTimeout(afterFocus, 1000);          } else {              const layoutdoc = Doc.Layout(doc); -            const newPanX = NumCast(doc.x) + NumCast(layoutdoc._width) / 2; -            const newPanY = NumCast(doc.y) + NumCast(layoutdoc._height) / 2; +            const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition }; + +            willZoom && this.setScaleToZoom(layoutdoc, scale); +            const newPanX = (NumCast(doc.x) + doc[WidthSym]() / 2) - (this.isAnnotationOverlay ? (Doc.NativeWidth(this.props.Document)) / 2 / this.zoomScaling() : 0); +            const newPanY = (NumCast(doc.y) + doc[HeightSym]() / 2) - (this.isAnnotationOverlay ? (Doc.NativeHeight(this.props.Document)) / 2 / this.zoomScaling() : 0);              const newState = HistoryUtil.getState();              newState.initializers![this.Document[Id]] = { panX: newPanX, panY: newPanY };              HistoryUtil.pushState(newState); -            const savedState = { px: this.Document._panX, py: this.Document._panY, s: this.Document[this.scaleFieldKey], pt: this.Document._viewTransition }; - -            if (DocListCast(this.dataDoc[this.props.fieldKey]).includes(doc)) { +            if (DocListCast(this.dataDoc[this.props.annotationsKey || this.props.fieldKey]).includes(doc)) {                  // glr: freeform transform speed can be set by adjusting presTransition field - needs a way of knowing when presentation is not active...                  if (!doc.z) this.setPan(newPanX, newPanY, doc.focusSpeed || doc.focusSpeed === 0 ? `transform ${doc.focusSpeed}ms` : "transform 500ms", true); // docs that are floating in their collection can't be panned to from their collection -- need to propagate the pan to a parent freeform somehow              }              Doc.BrushDoc(this.props.Document); -            this.props.focus(this.props.Document); -            willZoom && this.setScaleToZoom(layoutdoc, scale); -            Doc.linkFollowHighlight(doc); -            afterFocus && setTimeout(() => { -                if (afterFocus?.()) { -                    this.Document._panX = savedState.px; -                    this.Document._panY = savedState.py; -                    this.Document[this.scaleFieldKey] = savedState.s; -                    this.Document._viewTransition = savedState.pt; -                } -            }, 500); +            const newDidFocus = didFocus || (newPanX !== savedState.px || newPanY !== savedState.py); + +            const newAfterFocus = (didFocus: boolean) => { +                afterFocus && setTimeout(() => { +                    // @ts-ignore +                    if (afterFocus?.(didFocus || (newPanX !== savedState.px || newPanY !== savedState.py))) { +                        this.Document._panX = savedState.px; +                        this.Document._panY = savedState.py; +                        this.Document[this.scaleFieldKey] = savedState.s; +                        this.Document._viewTransition = savedState.pt; +                    } +                }, newPanX !== savedState.px || newPanY !== savedState.py ? 500 : 0); +                return false; +            }; +            this.props.focus(this.props.Document, undefined, undefined, newAfterFocus, undefined, newDidFocus); +            Doc.linkFollowHighlight(doc);          }      }      setScaleToZoom = (doc: Doc, scale: number = 0.75) => { -        this.Document[this.scaleFieldKey] = scale * Math.min(this.props.PanelWidth() / NumCast(doc._width), this.props.PanelHeight() / NumCast(doc._height)); +        const pw = this.isAnnotationOverlay ? Doc.NativeWidth(this.props.Document) : this.props.PanelWidth(); +        const ph = this.isAnnotationOverlay ? Doc.NativeHeight(this.props.Document) : this.props.PanelHeight(); +        pw && ph && (this.Document[this.scaleFieldKey] = scale * Math.min(pw / NumCast(doc._width), ph / NumCast(doc._height)));      }      @computed get libraryPath() { return this.props.LibraryPath ? [...this.props.LibraryPath, this.props.Document] : []; } @@ -949,7 +994,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P              LayoutTemplate: childLayout.z ? undefined : this.props.ChildLayoutTemplate,              LayoutTemplateString: childLayout.z ? undefined : this.props.ChildLayoutString,              FreezeDimensions: this.props.freezeChildDimensions, -            layoutKey: StrCast(this.props.Document.childLayoutKey),              setupDragLines: this.setupDragLines,              dontRegisterView: this.props.dontRegisterView,              rootSelected: childData ? this.rootSelected : returnFalse, @@ -964,6 +1008,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P              ContainingCollectionView: this.props.CollectionView,              ContainingCollectionDoc: this.props.Document,              docFilters: this.docFilters, +            docRangeFilters: this.docRangeFilters,              searchFilterDocs: this.searchFilterDocs,              focus: this.focusDocument,              backgroundColor: this.getClusterColor, @@ -1105,7 +1150,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P          });          this._cachedPool.clear();          Array.from(newPool.entries()).forEach(k => this._cachedPool.set(k[0], k[1])); -        const elements: ViewDefResult[] = computedElementData.slice(); +        const elements = computedElementData.slice();          const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine);          Array.from(newPool.entries()).filter(entry => this.isCurrent(entry[1].pair.layout)).forEach(entry =>              elements.push({ @@ -1126,8 +1171,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P                  bounds: this.childDataProvider(entry[1].pair.layout, entry[1].replica)              })); -        if (this.props.isAnnotationOverlay) { -            this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey])); +        if (this.props.isAnnotationOverlay) {   // don't zoom out farther than 1-1 if it's a bounded item (image, video, pdf), otherwise don't allow zooming in closer than 1-1 if it's a text sidebar +            if (this.props.scaleField) this.props.Document[this.scaleFieldKey] = Math.min(1, NumCast(this.props.Document[this.scaleFieldKey], 1)); +            else this.props.Document[this.scaleFieldKey] = Math.max(1, NumCast(this.props.Document[this.scaleFieldKey]));          }          this.Document._useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); @@ -1140,12 +1186,22 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P          this._layoutComputeReaction = reaction(() => this.doLayoutComputation,              (elements) => this._layoutElements = elements || [],              { fireImmediately: true, name: "doLayout" }); +        if (!this.props.annotationsKey) { +            this._boundsReaction = reaction(() => this.contentBounds, +                bounds => (!this.fitToContent && this._layoutElements?.length) && setTimeout(() => { +                    const rbounds = Cast(this.Document._renderContentBounds, listSpec("number"), [0, 0, 0, 0]); +                    if (rbounds[0] !== bounds.x || rbounds[1] !== bounds.y || rbounds[2] !== bounds.r || rbounds[3] !== bounds.b) { +                        this.Document._renderContentBounds = new List<number>([bounds.x, bounds.y, bounds.r, bounds.b]); +                    } +                })); +        }          this._marqueeRef.current?.addEventListener("dashDragAutoScroll", this.onDragAutoScroll as any);      }      componentWillUnmount() {          this._layoutComputeReaction?.(); +        this._boundsReaction?.();          this._marqueeRef.current?.removeEventListener("dashDragAutoScroll", this.onDragAutoScroll as any);      } @@ -1154,7 +1210,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P      @action      onCursorMove = (e: React.PointerEvent) => { -        super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); +        //  super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));      } @@ -1165,7 +1221,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P          if ((e as any).handlePan || this.props.isAnnotationOverlay) return;          (e as any).handlePan = true; -        if (!this.props.Document._noAutoscroll && this._marqueeRef?.current) { +        if (!this.props.Document._noAutoscroll && !this.props.renderDepth && this._marqueeRef?.current) {              const dragX = e.detail.clientX;              const dragY = e.detail.clientY;              const bounds = this._marqueeRef.current?.getBoundingClientRect(); @@ -1255,7 +1311,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P          optionItems.push({ description: this.layoutDoc._lockedTransform ? "Unlock Transform" : "Lock Transform", event: this.toggleLockTransform, icon: this.layoutDoc._lockedTransform ? "unlock" : "lock" });          this.props.renderDepth && optionItems.push({ description: "Use Background Color as Default", event: () => Cast(Doc.UserDoc().emptyCollection, Doc, null)._backgroundColor = StrCast(this.layoutDoc._backgroundColor), icon: "palette" });          if (!Doc.UserDoc().noviceMode) { -            optionItems.push({ description: (!this.layoutDoc._nativeWidth || !this.layoutDoc._nativeHeight ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" }); +            optionItems.push({ description: (!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? "Freeze" : "Unfreeze") + " Aspect", event: this.toggleNativeDimensions, icon: "snowflake" });              optionItems.push({ description: `${this.Document._freeformLOD ? "Enable LOD" : "Disable LOD"}`, event: () => this.Document._freeformLOD = !this.Document._freeformLOD, icon: "table" });          } @@ -1381,6 +1437,44 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P          }          return false;      }); + +    chooseGridSpace = (gridSpace: number): number => { +        const divisions = this.props.PanelWidth() / this.zoomScaling() / gridSpace + 3; +        return divisions < 60 ? gridSpace : this.chooseGridSpace(gridSpace * 10); +    } + +    @computed get grid() { +        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; +        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.centeringShiftX() % renderGridSpace; +                    const Cy = this.centeringShiftY() % 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 = "rgba(0, 0, 0, 0.5)"; +                        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 marqueeView() {          return <MarqueeView              {...this.props} @@ -1394,6 +1488,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P              getTransform={this.getTransform}              isAnnotationOverlay={this.isAnnotationOverlay}>              <div ref={this._marqueeRef}> +                {this.layoutDoc["_backgroundGrid-show"] ? this.grid : (null)}                  <CollectionFreeFormViewPannableContents                      centeringShiftX={this.centeringShiftX}                      centeringShiftY={this.centeringShiftY} @@ -1410,19 +1505,20 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P          </MarqueeView>;      } +      @computed get contentScaling() {          if (this.props.annotationsKey && !this.props.forceScaling) return 0; -        const nw = returnVal(this.props.NativeWidth?.(), NumCast(this.Document._nativeWidth)); -        const nh = returnVal(this.props.NativeHeight?.(), NumCast(this.Document._nativeHeight)); +        const nw = returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.Document)); +        const nh = returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.Document));          const hscale = nh ? this.props.PanelHeight() / nh : 1;          const wscale = nw ? this.props.PanelWidth() / nw : 1;          return wscale < hscale ? wscale : hscale;      }      @computed get backgroundEvents() { return this.layoutDoc._isBackground && SnappingManager.GetIsDragging(); } +      render() {          TraceMobx();          const clientRect = this._mainCont?.getBoundingClientRect(); -        !this.fitToContent && this._layoutElements?.length && setTimeout(() => this.Document._renderContentBounds = new List<number>([this.contentBounds.x, this.contentBounds.y, this.contentBounds.r, this.contentBounds.b]), 0);          return <div className={"collectionfreeformview-container"} ref={this.createDashEventsTarget}              onPointerOver={this.onPointerOver}              onWheel={this.onPointerWheel} @@ -1582,7 +1678,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF      }      @computed get zoomProgressivize() { -        return PresBox.Instance && PresBox.Instance.activeItem && PresBox.Instance.activeItem.presPinView && PresBox.Instance.layoutDoc.presStatus === 'edit' ? this.zoomProgressivizeContainer : (null); +        return PresBox.Instance?.activeItem?.presPinView && PresBox.Instance.layoutDoc.presStatus === 'edit' ? this.zoomProgressivizeContainer : (null);      }      @computed get progressivize() { @@ -1613,6 +1709,7 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF          </>;      } +      render() {          // trace();          const freeformclass = "collectionfreeformview" + (this.props.viewDefDivClick ? "-viewDef" : "-none"); @@ -1622,6 +1719,12 @@ class CollectionFreeFormViewPannableContents extends React.Component<CollectionF          const pany = -this.props.panY();          const zoom = this.props.zoomScaling();          return <div className={freeformclass} +            onScroll={e => { +                const target = e.target as any; +                if (getComputedStyle(target)?.overflow === "visible") {  // if collection is visible, then scrolling will mess things up since there are no scroll bars +                    target.scrollTop = target.scrollLeft = 0; +                } +            }}              style={{                  transform: `translate(${cenx}px, ${ceny}px) scale(${zoom}) translate(${panx}px, ${pany}px)`,                  transition: this.props.transition, | 
