diff options
| author | Sophie Zhang <sophie_zhang@brown.edu> | 2024-01-25 11:35:26 -0500 |
|---|---|---|
| committer | Sophie Zhang <sophie_zhang@brown.edu> | 2024-01-25 11:35:26 -0500 |
| commit | f3dab2a56db5e4a6a3dca58185d94e1ff7d1dc32 (patch) | |
| tree | a7bc895266b53bb620dbd2dd71bad2e83b555446 /src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | |
| parent | b5c5410b4af5d2c68d2107d3f064f6e3ec4ac3f2 (diff) | |
| parent | 136f3d9f349d54e8bdd73b6380ea47c19e5edebf (diff) | |
Merge branch 'master' into sophie-ai-images
Diffstat (limited to 'src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx')
| -rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 1400 |
1 files changed, 560 insertions, 840 deletions
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 2ac8f6291..818c754c3 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,28 +1,27 @@ import { Bezier } from 'bezier-js'; import { Colors } from 'browndash-components'; -import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; +import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; +import * as React from 'react'; import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; import { DocData, Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkField, InkTool, PointData, Segment } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; -import { RichTextField } from '../../../../fields/RichTextField'; import { listSpec } from '../../../../fields/Schema'; import { ScriptField } from '../../../../fields/ScriptField'; -import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { TraceMobx } from '../../../../fields/util'; import { GestureUtils } from '../../../../pen-gestures/GestureUtils'; -import { aggregateBounds, DashColor, emptyFunction, intersectRect, lightOrDark, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { aggregateBounds, DashColor, emptyFunction, intersectRect, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, Utils } from '../../../../Utils'; import { CognitiveServices } from '../../../cognitive_services/CognitiveServices'; import { Docs, DocUtils } from '../../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager, dropActionType } from '../../../util/DragManager'; -import { InteractionUtils } from '../../../util/InteractionUtils'; import { FollowLinkScript } from '../../../util/LinkFollower'; import { ReplayMovements } from '../../../util/ReplayMovements'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; @@ -31,34 +30,31 @@ import { freeformScrollMode, SettingsManager } from '../../../util/SettingsManag import { SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; import { undoBatch, UndoManager } from '../../../util/UndoManager'; -import { COLLECTION_BORDER_WIDTH } from '../../../views/global/globalCssVariables.scss'; import { Timeline } from '../../animationtimeline/Timeline'; import { ContextMenu } from '../../ContextMenu'; import { GestureOverlay } from '../../GestureOverlay'; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; +import { CtrlKey } from '../../GlobalKeyHandler'; +import { ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; import { LightboxView } from '../../LightboxView'; -import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView'; -import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from '../../nodes/DocumentView'; -import { FieldViewProps } from '../../nodes/FieldView'; +import { CollectionFreeFormDocumentView, CollectionFreeFormDocumentViewWrapper } from '../../nodes/CollectionFreeFormDocumentView'; +import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp'; +import { DocumentView, OpenWhere } from '../../nodes/DocumentView'; +import { FocusViewOptions, FieldViewProps } from '../../nodes/FieldView'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { PinProps, PresBox } from '../../nodes/trails/PresBox'; import { CreateImage } from '../../nodes/WebBoxRenderer'; import { StyleProp } from '../../StyleProvider'; import { CollectionSubView } from '../CollectionSubView'; import { TreeViewType } from '../CollectionTreeView'; -import { TabDocView } from '../TabDocView'; +import { CollectionFreeFormBackgroundGrid } from './CollectionFreeFormBackgroundGrid'; +import { CollectionFreeFormInfoUI } from './CollectionFreeFormInfoUI'; 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'; -import React = require('react'); -import { DocumentWithColor, GeneratedResponse, generatePalette, StyleInput, StyleInputDocument } from '../../../apis/gpt/customization'; -import { PropertiesView } from '../../PropertiesView'; -import { MainView } from '../../MainView'; -import { ExtractColors } from '../../ExtractColors'; -import { extname } from 'path'; - -export type collectionFreeformViewProps = { + +export interface collectionFreeformViewProps { NativeWidth?: () => number; NativeHeight?: () => number; originTopLeft?: boolean; @@ -69,16 +65,18 @@ export type collectionFreeformViewProps = { noOverlay?: boolean; // used to suppress docs in the overlay (z) layer (ie, for minimap since overlay doesn't scale) engineProps?: any; getScrollHeight?: () => number | undefined; - dontRenderDocuments?: boolean; // used for annotation overlays which need to distribute documents into different freeformviews with different mixBlendModes depending on whether they are transparent or not. - // However, this screws up interactions since only the top layer gets events. so we render the freeformview a 3rd time with all documents in order to get interaction events (eg., marquee) but we don't actually want to display the documents. -}; +} @observer export class CollectionFreeFormView extends CollectionSubView<Partial<collectionFreeformViewProps>>() { public get displayName() { - return 'CollectionFreeFormView(' + this.props.Document.title?.toString() + ')'; + return 'CollectionFreeFormView(' + this.Document.title?.toString() + ')'; } // this makes mobx trace() statements more descriptive + constructor(props: any) { + super(props); + makeObservable(this); + } @observable public static ShowPresPaths = false; @@ -88,37 +86,28 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection private _downX: number = 0; private _downY: number = 0; private _downTime = 0; - private _inkToTextStartX: number | undefined; - private _inkToTextStartY: number | undefined; - private _wordPalette: Map<string, string> = new Map<string, string>(); private _clusterDistance: number = 75; private _hitCluster: number = -1; private _disposers: { [name: string]: IReactionDisposer } = {}; private _renderCutoffData = observable.map<string, boolean>(); - private _layoutPoolData = observable.map<string, PoolData>(); - private _layoutSizeData = observable.map<string, { width?: number; height?: number }>(); - private _cachedPool: Map<string, PoolData> = new Map(); private _batch: UndoManager.Batch | undefined = undefined; private _brushtimer: any; private _brushtimer1: any; public get isAnnotationOverlay() { - return this.props.isAnnotationOverlay; + return this._props.isAnnotationOverlay; } public get scaleFieldKey() { - return (this.props.viewField ?? '') + '_freeform_scale'; + return (this._props.viewField ?? '') + '_freeform_scale'; } private get panXFieldKey() { - return (this.props.viewField ?? '') + '_freeform_panX'; + return (this._props.viewField ?? '') + '_freeform_panX'; } private get panYFieldKey() { - return (this.props.viewField ?? '') + '_freeform_panY'; + return (this._props.viewField ?? '') + '_freeform_panY'; } private get autoResetFieldKey() { - return (this.props.viewField ?? '') + '_freeform_autoReset'; - } - private get borderWidth() { - return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; + return (this._props.viewField ?? '') + '_freeform_autoReset'; } @observable.shallow _layoutElements: ViewDefResult[] = []; // shallow because some layout items (eg pivot labels) are just generated 'divs' and can't be frozen as observables @@ -128,31 +117,32 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _clusterSets: Doc[][] = []; @observable _deleteList: DocumentView[] = []; @observable _timelineRef = React.createRef<Timeline>(); - @observable _marqueeRef: HTMLDivElement | null = null; @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 _brushedView: { width: number; height: number; panX: number; panY: number } | undefined; // highlighted region of freeform canvas used by presentations to indicate a region - @computed get views() { + @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); if (viewsMask.length) renderableEles.push(<div className={`collectionfreeformview-mask${this._layoutElements.some(ele => (ele.inkMask ?? 0) > 0) ? '' : '-empty'}`}>{viewsMask}</div>); return renderableEles; } @computed get fitToContentVals() { + const hgt = this.contentBounds.b - this.contentBounds.y; + const wid = this.contentBounds.r - this.contentBounds.x; return { - bounds: { ...this.contentBounds, cx: (this.contentBounds.x + this.contentBounds.r) / 2, cy: (this.contentBounds.y + this.contentBounds.b) / 2 }, + bounds: { ...this.contentBounds, cx: this.contentBounds.x + wid / 2, cy: this.contentBounds.y + hgt / 2 }, scale: - !this.childDocs.length || !Number.isFinite(this.contentBounds.b - this.contentBounds.y) || !Number.isFinite(this.contentBounds.r - this.contentBounds.x) - ? 1 - : Math.min(this.props.PanelHeight() / (this.contentBounds.b - this.contentBounds.y), this.props.PanelWidth() / (this.contentBounds.r - this.contentBounds.x)), + (!this.childDocs.length || !Number.isFinite(hgt) || !Number.isFinite(wid) + ? 1 // + : Math.min(this._props.PanelHeight() / hgt, this._props.PanelWidth() / wid)) / (this._props.NativeDimScaling?.() || 1), }; } @computed get fitContentsToBox() { - return (this.props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox) && !this.isAnnotationOverlay; + return (this._props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox) && !this.isAnnotationOverlay; } @computed get contentBounds() { - const cb = Cast(this.rootDoc.contentBounds, listSpec('number')); + const cb = Cast(this.dataDoc.contentBounds, listSpec('number')); return cb ? { x: cb[0], y: cb[1], r: cb[2], b: cb[3] } : aggregateBounds( @@ -162,56 +152,50 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ); } @computed get nativeWidth() { - return this.props.NativeWidth?.() || (this.fitContentsToBox ? 0 : Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null))); + return this._props.NativeWidth?.() || Doc.NativeWidth(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)); } @computed get nativeHeight() { - return this.props.NativeHeight?.() || (this.fitContentsToBox ? 0 : Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null))); + return this._props.NativeHeight?.() || Doc.NativeHeight(this.Document, Cast(this.Document.resolvedDataDoc, Doc, null)); } @computed get cachedCenteringShiftX(): number { - const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling; - return this.props.isAnnotationOverlay || this.props.originTopLeft ? 0 : this.props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections + const scaling = !this.nativeDimScaling ? 1 : this.nativeDimScaling; + return this._props.isAnnotationOverlay || this._props.originTopLeft ? 0 : this._props.PanelWidth() / 2 / scaling; // shift so pan position is at center of window for non-overlay collections } @computed get cachedCenteringShiftY(): number { - const dv = this.props.DocumentView?.(); - const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling; + const dv = this.DocumentView?.(); + const fitWidth = this._props.layout_fitWidth?.(this.Document) ?? dv?.layoutDoc.layout_fitWidth; + const scaling = !this.nativeDimScaling ? 1 : this.nativeDimScaling; // if freeform has a native aspect, then the panel height needs to be adjusted to match it - const aspect = dv?.nativeWidth && dv?.nativeHeight && !dv.layoutDoc.layout_fitWidth ? dv.nativeHeight / dv.nativeWidth : this.props.PanelHeight() / this.props.PanelWidth(); - return this.props.isAnnotationOverlay || this.props.originTopLeft ? 0 : (aspect * this.props.PanelWidth()) / 2 / scaling; // 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()); + const aspect = dv?.nativeWidth && dv?.nativeHeight && !fitWidth ? dv.nativeHeight / dv.nativeWidth : this._props.PanelHeight() / this._props.PanelWidth(); + return this._props.isAnnotationOverlay || this._props.originTopLeft ? 0 : (aspect * this._props.PanelWidth()) / 2 / scaling; // shift so pan position is at center of window for non-overlay collections } - @computed get cachedGetContainerTransform(): Transform { - return this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth); + @computed get panZoomXf() { + return new Transform(this.panX(), this.panY(), 1 / this.zoomScaling()); } - @computed get cachedGetTransform(): Transform { - return this.getContainerTransform() - .scale(this.props.isAnnotationOverlay ? 1 : 1 / this.nativeDim()) + @computed get screenToFreeformContentsXf() { + return this._props + .ScreenToLocalTransform() // .translate(-this.cachedCenteringShiftX, -this.cachedCenteringShiftY) - .transform(this.cachedGetLocalTransform); + .transform(this.panZoomXf); } public static gotoKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], duration: number) { - if (timer) clearTimeout(timer); - return DocumentView.SetViewTransition(docs, 'all', duration, undefined, true); + return DocumentView.SetViewTransition(docs, 'all', duration, timer, undefined, true); } public static updateKeyframe(timer: NodeJS.Timeout | undefined, docs: Doc[], time: number) { - if (timer) clearTimeout(timer); - const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, undefined, true); + const newTimer = DocumentView.SetViewTransition(docs, 'all', 1000, timer, undefined, true); const timecode = Math.round(time); docs.forEach(doc => { CollectionFreeFormDocumentView.animFields.forEach(val => { - const findexed = Cast(doc[`${val.key}-indexed`], listSpec('number'), null); + const findexed = Cast(doc[`${val.key}_indexed`], listSpec('number'), null); findexed?.length <= timecode + 1 && findexed.push(undefined as any as number); }); CollectionFreeFormDocumentView.animStringFields.forEach(val => { - const findexed = Cast(doc[`${val}-indexed`], listSpec('string'), null); + const findexed = Cast(doc[`${val}_indexed`], listSpec('string'), null); findexed?.length <= timecode + 1 && findexed.push(undefined as any as string); }); CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => { - const findexed = Cast(doc[`${val}-indexed`], listSpec(InkField), null); + const findexed = Cast(doc[`${val}_indexed`], listSpec(InkField), null); findexed?.length <= timecode + 1 && findexed.push(undefined as any); }); }); @@ -237,48 +221,44 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _keyframeEditing = false; @action setKeyFrameEditing = (set: boolean) => (this._keyframeEditing = set); getKeyFrameEditing = () => this._keyframeEditing; - onBrowseClickHandler = () => this.props.onBrowseClick?.() || ScriptCast(this.layoutDoc.onBrowseClick); - onChildClickHandler = () => this.props.childClickScript || ScriptCast(this.Document.onChildClick); - onChildDoubleClickHandler = () => this.props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); + onBrowseClickHandler = () => this._props.onBrowseClickScript?.() || ScriptCast(this.layoutDoc.onBrowseClick); + onChildClickHandler = () => this._props.childClickScript || ScriptCast(this.Document.onChildClick); + onChildDoubleClickHandler = () => this._props.childDoubleClickScript || ScriptCast(this.Document.onChildDoubleClick); elementFunc = () => this._layoutElements; fitContentOnce = () => { - if (this.props.DocumentView?.().nativeWidth) return; const vals = this.fitToContentVals; this.layoutDoc._freeform_panX = vals.bounds.cx; this.layoutDoc._freeform_panY = vals.bounds.cy; this.layoutDoc._freeform_scale = vals.scale; }; freeformData = (force?: boolean) => (!this._firstRender && (this.fitContentsToBox || force) ? this.fitToContentVals : undefined); - reverseNativeScaling = () => (this.fitContentsToBox ? true : false); // freeform_panx, freeform_pany, freeform_scale all attempt to get values first from the layout controller, then from the layout/dataDoc (or template layout doc), and finally from the resolved template data document. // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panX, 1)); panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panY, 1)); zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.[this.scaleFieldKey], 1)); - contentTransform = () => - this.props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`; - getTransform = () => this.cachedGetTransform.copy(); - getLocalTransform = () => this.cachedGetLocalTransform.copy(); - getContainerTransform = () => this.cachedGetContainerTransform.copy(); + PanZoomCenterXf = () => + this._props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`; + ScreenToContentsXf = () => this.screenToFreeformContentsXf.copy(); getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout); - isAnyChildContentActive = () => this.props.isAnyChildContentActive(); - addLiveTextBox = (newBox: Doc) => { - FormattedTextBox.SelectOnLoad = newBox[Id]; // track the new text box so we can give it a prop that tells it to focus itself when it's displayed - this.addDocument(newBox); + isAnyChildContentActive = () => this._props.isAnyChildContentActive(); + addLiveTextBox = (newDoc: Doc) => { + FormattedTextBox.SetSelectOnLoad(newDoc); // track the new text box so we can give it a prop that tells it to focus itself when it's displayed + this.addDocument(newDoc); }; selectDocuments = (docs: Doc[]) => { SelectionManager.DeselectAll(); - docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.DocumentView?.())).map(dv => dv && SelectionManager.SelectView(dv, true)); + docs.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).forEach(dv => dv && SelectionManager.SelectView(dv, true)); }; addDocument = (newBox: Doc | Doc[]) => { let retVal = false; if (newBox instanceof Doc) { - if ((retVal = this.props.addDocument?.(newBox) || false)) { + if ((retVal = this._props.addDocument?.(newBox) || false)) { this.bringToFront(newBox); this.updateCluster(newBox); } } else { - retVal = this.props.addDocument?.(newBox) || false; + retVal = this._props.addDocument?.(newBox) || false; // bcz: deal with clusters } if (retVal) { @@ -286,13 +266,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection for (const newBox of newBoxes) { if (newBox.activeFrame !== undefined) { const vals = CollectionFreeFormDocumentView.animFields.map(field => newBox[field.key]); - CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[`${field.key}-indexed`]); + CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[`${field.key}_indexed`]); CollectionFreeFormDocumentView.animFields.forEach(field => delete newBox[field.key]); delete newBox.activeFrame; CollectionFreeFormDocumentView.animFields.forEach((field, i) => field.key !== 'opacity' && (newBox[field.key] = vals[i])); } } - if (this.Document._currentFrame !== undefined && !this.props.isAnnotationOverlay) { + if (this.Document._currentFrame !== undefined && !this._props.isAnnotationOverlay) { CollectionFreeFormDocumentView.setupKeyframes(newBoxes, NumCast(this.Document._currentFrame), true); } } @@ -306,16 +286,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return dispTime === -1 || curTime === -1 || (curTime - dispTime >= -1e-4 && curTime <= endTime); } - groupFocus = (anchor: Doc, options: DocFocusOptions) => { - options.docTransform = new Transform(-NumCast(this.rootDoc[this.panXFieldKey]) + NumCast(anchor.x), -NumCast(this.rootDoc[this.panYFieldKey]) + NumCast(anchor.y), 1); - const res = this.props.focus(this.rootDoc, options); + groupFocus = (anchor: Doc, options: FocusViewOptions) => { + options.docTransform = new Transform(-NumCast(this.layoutDoc[this.panXFieldKey]) + NumCast(anchor.x), -NumCast(this.layoutDoc[this.panYFieldKey]) + NumCast(anchor.y), 1); + const res = this._props.focus(this.Document, options); options.docTransform = undefined; return res; }; - focus = (anchor: Doc, options: DocFocusOptions) => { + focus = (anchor: Doc, options: FocusViewOptions) => { if (this._lightboxDoc) return; - if (anchor === this.rootDoc) { + if (anchor === this.Document) { if (options.willZoomCentered && options.zoomScale) { this.fitContentOnce(); options.didMove = true; @@ -324,7 +304,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (anchor.type !== DocumentType.CONFIG && !DocListCast(this.Document[this.fieldKey ?? Doc.LayoutFieldKey(this.Document)]).includes(anchor)) return; const xfToCollection = options?.docTransform ?? Transform.Identity(); const savedState = { panX: NumCast(this.Document[this.panXFieldKey]), panY: NumCast(this.Document[this.panYFieldKey]), scale: options?.willZoomCentered ? this.Document[this.scaleFieldKey] : undefined }; - const cantTransform = this.fitContentsToBox || ((this.rootDoc._isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc); + const cantTransform = this.fitContentsToBox || ((this.Document.isGroup || this.layoutDoc._lockedTransform) && !LightboxView.LightboxDoc); const { panX, panY, scale } = cantTransform || (!options.willPan && !options.willZoomCentered) ? savedState : this.calculatePanIntoView(anchor, xfToCollection, options?.willZoomCentered ? options?.zoomScale ?? 0.75 : undefined); // focus on the document in the collection @@ -339,22 +319,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } }; - getView = async (doc: Doc): Promise<Opt<DocumentView>> => { - return new Promise<Opt<DocumentView>>(res => { - if (doc.hidden && this._lightboxDoc !== doc) doc.hidden = false; + getView = async (doc: Doc, options: FocusViewOptions): Promise<Opt<DocumentView>> => + new Promise<Opt<DocumentView>>(res => { + if (doc.hidden && this._lightboxDoc !== doc) options.didMove = !(doc.hidden = false); const findDoc = (finish: (dv: DocumentView) => void) => DocumentManager.Instance.AddViewRenderedCb(doc, dv => finish(dv)); findDoc(dv => res(dv)); }); - }; - @action - internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData, xp: number, yp: number) { + internalDocDrop(e: Event, de: DragManager.DropEvent, docDragData: DragManager.DocumentDragData) { if (!super.onInternalDrop(e, de)) return false; const refDoc = docDragData.droppedDocuments[0]; - const [xpo, ypo] = this.getContainerTransform().transformPoint(de.x, de.y); - const z = NumCast(refDoc.z); - const x = (z ? xpo : xp) - docDragData.offset[0]; - const y = (z ? ypo : yp) - docDragData.offset[1]; + const fromScreenXf = NumCast(refDoc.z) ? this.ScreenToLocalBoxXf() : this.screenToFreeformContentsXf; + const [xpo, ypo] = fromScreenXf.transformPoint(de.x, de.y); + const x = xpo - docDragData.offset[0]; + const y = ypo - docDragData.offset[1]; const zsorted = this.childLayoutPairs .map(pair => pair.layout) .slice() @@ -366,16 +344,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection for (let i = 0; i < docDragData.droppedDocuments.length; i++) { const d = docDragData.droppedDocuments[i]; const layoutDoc = Doc.Layout(d); + const delta = Utils.rotPt(x - dropPos[0], y - dropPos[1], fromScreenXf.Rotate); if (this.Document._currentFrame !== undefined) { CollectionFreeFormDocumentView.setupKeyframes([d], NumCast(this.Document._currentFrame), false); const pvals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000)); // get filled in values (uses defaults when not value is specified) for position const vals = CollectionFreeFormDocumentView.getValues(d, NumCast(d.activeFrame, 1000), false); // get non-default values for everything else - vals.x = x + NumCast(pvals.x) - dropPos[0]; - vals.y = y + NumCast(pvals.y) - dropPos[1]; + vals.x = NumCast(pvals.x) + delta.x; + vals.y = NumCast(pvals.y) + delta.y; CollectionFreeFormDocumentView.setValues(NumCast(this.Document._currentFrame), d, vals); } else { - d.x = x + NumCast(d.x) - dropPos[0]; - d.y = y + NumCast(d.y) - dropPos[1]; + d.x = NumCast(d.x) + delta.x; + d.y = NumCast(d.y) + delta.y; } d._layout_modificationDate = new DateField(); const nd = [Doc.NativeWidth(layoutDoc), Doc.NativeHeight(layoutDoc)]; @@ -411,8 +390,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } @undoBatch - internalAnchorAnnoDrop(e: Event, annoDragData: DragManager.AnchorAnnoDragData, xp: number, yp: number) { + internalAnchorAnnoDrop(e: Event, de: DragManager.DropEvent, annoDragData: DragManager.AnchorAnnoDragData) { const dropCreator = annoDragData.dropDocCreator; + const [xp, yp] = this.screenToFreeformContentsXf.transformPoint(de.x, de.y); annoDragData.dropDocCreator = (annotationOn: Doc | undefined) => { const dropDoc = dropCreator(annotationOn); if (dropDoc) { @@ -420,26 +400,27 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection dropDoc.y = yp - annoDragData.offset[1]; this.bringToFront(dropDoc); } - return dropDoc || this.rootDoc; + return dropDoc || this.Document; }; return true; } @undoBatch - internalLinkDrop(e: Event, de: DragManager.DropEvent, linkDragData: DragManager.LinkDragData, xp: number, yp: number) { - if (linkDragData.linkDragView.props.docViewPath().includes(this.props.docViewPath().lastElement())) { + internalLinkDrop(e: Event, de: DragManager.DropEvent, linkDragData: DragManager.LinkDragData) { + if (this.DocumentView?.() && linkDragData.linkDragView.containerViewPath?.().includes(this.DocumentView())) { + const [x, y] = this.screenToFreeformContentsXf.transformPoint(de.x, de.y); let added = false; // do nothing if link is dropped into any freeform view parent of dragged document const source = - !linkDragData.dragDocument.embedContainer || linkDragData.dragDocument.embedContainer !== this.rootDoc - ? Docs.Create.TextDocument('', { _width: 200, _height: 75, x: xp, y: yp, title: 'dropped annotation' }) + !linkDragData.dragDocument.embedContainer || linkDragData.dragDocument.embedContainer !== this.Document + ? Docs.Create.TextDocument('', { _width: 200, _height: 75, x, y, title: 'dropped annotation' }) : Docs.Create.FontIconDocument({ title: 'anchor', icon_label: '', followLinkToggle: true, icon: 'map-pin', - x: xp, - y: yp, + x, + y, backgroundColor: '#ACCEF7', layout_hideAllLinks: true, layout_hideLinkButton: true, @@ -448,7 +429,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection _xPadding: 0, onClick: FollowLinkScript(), }); - added = this.props.addDocument?.(source) ? true : false; + added = this._props.addDocument?.(source) ? true : false; de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { link_relationship: 'annotated by:annotation of' }); // TODODO this is where in text links get passed e.stopPropagation(); @@ -459,20 +440,32 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } onInternalDrop = (e: Event, de: DragManager.DropEvent) => { - const [xp, yp] = this.getTransform().transformPoint(de.x, de.y); - if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData, xp, yp); - else if (de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData, xp, yp); - else if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData, xp, yp); + if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de, de.complete.annoDragData); + else if (de.complete.linkDragData) return this.internalLinkDrop(e, de, de.complete.linkDragData); + else if (de.complete.docDragData?.droppedDocuments.length) return this.internalDocDrop(e, de, de.complete.docDragData); return false; }; - onExternalDrop = (e: React.DragEvent) => (([x, y]) => super.onExternalDrop(e, { x, y }))(this.getTransform().transformPoint(e.pageX, e.pageY)); - + 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.props.Document._freeform_useClusters ? NumCast(cd.layout_cluster, -1) : NumCast(cd.group, -1); + 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; @@ -485,16 +478,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }, -1); } - tryDragCluster(e: PointerEvent | TouchEvent, cluster: number) { + tryDragCluster(e: PointerEvent, cluster: number) { if (cluster !== -1) { - const ptsParent = e instanceof PointerEvent ? e : e.targetTouches.item(0); + const ptsParent = e; if (ptsParent) { - const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === cluster); - const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.DocumentView?.())!); - const { left, top } = clusterDocs[0].getBounds() || { left: 0, top: 0 }; + 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 ? 'embed' : undefined); - de.moveDocument = this.props.moveDocument; - de.offset = this.getTransform().transformDirection(ptsParent.clientX - left, ptsParent.clientY - top); + de.moveDocument = this._props.moveDocument; + de.offset = this.screenToFreeformContentsXf.transformDirection(ptsParent.clientX - left, ptsParent.clientY - top); DragManager.StartDocumentDrag( clusterDocs.map(v => v.ContentDiv!), de, @@ -511,7 +504,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action updateClusters(_freeform_useClusters: boolean) { - this.props.Document._freeform_useClusters = _freeform_useClusters; + this.Document._freeform_useClusters = _freeform_useClusters; this._clusterSets.length = 0; this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c)); } @@ -519,7 +512,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action updateClusterDocs(docs: Doc[]) { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); - if (this.props.Document._freeform_useClusters) { + if (this.Document._freeform_useClusters) { const docFirst = docs[0]; docs.map(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1))); const preferredInd = NumCast(docFirst.layout_cluster); @@ -527,7 +520,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection docs.map(doc => this._clusterSets.map((set, i) => set.map(member => { - if (docFirst.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { + if (docFirst.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && CollectionFreeFormView.overlapping(doc, member, this._clusterDistance)) { docFirst.layout_cluster = i; } }) @@ -560,15 +553,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } @action - updateCluster(doc: Doc) { + updateCluster = (doc: Doc) => { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); - if (this.props.Document._freeform_useClusters) { + 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 && Doc.overlapping(doc, member, this._clusterDistance)) { + if (doc.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && CollectionFreeFormView.overlapping(doc, member, this._clusterDistance)) { doc.layout_cluster = i; } }) @@ -589,38 +582,39 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._clusterSets[doc.layout_cluster ?? 0].push(doc); } } - } + }; - getClusterColor = (doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string) => { - let styleProp = this.props.styleProvider?.(doc, props, property); // bcz: check 'props' used to be renderDepth + 1 - switch (property) { - 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?.map(s => (styleProp = StrCast(s.backgroundColor))); + 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) { + 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?.map(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; + case StyleProp.FillColor: + if (doc && this.Document._currentFrame !== undefined) { + return CollectionFreeFormDocumentView.getStringValues(doc, NumCast(this.Document._currentFrame))?.fillColor; + } + } return styleProp; }; trySelectCluster = (addToSel: boolean) => { if (this._hitCluster !== -1) { !addToSel && SelectionManager.DeselectAll(); - const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === this._hitCluster); + 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; } @@ -628,40 +622,26 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @action - onPenUp = (e: PointerEvent): void => { - if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { - document.removeEventListener('pointerup', this.onPenUp); - const currentCol = DocListCast(this.rootDoc.currentInkDoc); - const rootDocList = DocListCast(this.rootDoc.data); - currentCol.push(rootDocList[rootDocList.length - 1]); - - this._batch?.end(); - } - }; - - @action onPointerDown = (e: React.PointerEvent): void => { this._downX = this._lastX = e.pageX; this._downY = this._lastY = e.pageY; this._downTime = Date.now(); - if (e.button === 0 && !e.altKey && !e.ctrlKey && this.props.isContentActive(true)) { - if ( - !this.props.Document._isGroup && // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag - !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && - !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) - ) { + const scrollMode = e.altKey ? (Doc.UserDoc().freeformScrollMode === freeformScrollMode.Pan ? freeformScrollMode.Zoom : freeformScrollMode.Pan) : Doc.UserDoc().freeformScrollMode; + if (e.button === 0 && (!(e.ctrlKey && !e.metaKey) || scrollMode !== freeformScrollMode.Pan) && this._props.isContentActive()) { + if (!this.Document.isGroup) { + // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag // prettier-ignore switch (Doc.ActiveTool) { - case InkTool.Highlighter: break; + case InkTool.Highlighter: break; case InkTool.Write: break; - case InkTool.Pen: break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views + case InkTool.Pen: break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views case InkTool.Eraser: this._batch = UndoManager.StartBatch('collectionErase'); setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction); break; case InkTool.None: - if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) { - this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); + 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 ? true : false, false); } break; @@ -670,29 +650,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } }; - @action - handle1PointerDown = (e: React.TouchEvent, me: InteractionUtils.MultiTouchEvent<React.TouchEvent>) => { - // const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); - const pt = me.changedTouches[0]; - if (pt) { - this._hitCluster = this.pickCluster(this.getTransform().transformPoint(pt.clientX, pt.clientY)); - if (!e.shiftKey && !e.altKey && !e.ctrlKey && this.props.isContentActive(true)) { - this.removeMoveListeners(); - this.addMoveListeners(); - this.removeEndListeners(); - this.addEndListeners(); - if (Doc.ActiveTool === InkTool.None) { - this._lastX = pt.pageX; - this._lastY = pt.pageY; - e.preventDefault(); - e.stopPropagation(); - } else { - e.preventDefault(); - } - } - } - }; - public unprocessedDocs: Doc[] = []; public static collectionsWithUnprocessedInk = new Set<CollectionFreeFormView>(); @undoBatch @@ -705,25 +662,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection case GestureUtils.Gestures.Triangle: case GestureUtils.Gestures.Stroke: const points = ge.points; - const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); - console.log(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); + const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); + const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale; const inkDoc = Docs.Create.InkDocument( - ActiveInkColor(), - ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale, - ActiveInkBezierApprox(), - ActiveFillColor(), - ActiveArrowStart(), - ActiveArrowEnd(), - ActiveDash(), points, - ActiveIsInkMask(), - { - title: ge.gesture.toString(), - x: B.x - (ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale) / 2, - y: B.y - (ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale) / 2, - _width: B.width + ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale, - _height: B.height + ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale, - } + { title: ge.gesture.toString(), + x: B.x - inkWidth / 2, + y: B.y - inkWidth / 2, + _width: B.width + inkWidth, + _height: B.height + inkWidth }, // prettier-ignore + inkWidth ); if (Doc.ActiveTool === InkTool.Write) { this.unprocessedDocs.push(inkDoc); @@ -733,69 +681,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection e.stopPropagation(); break; case GestureUtils.Gestures.Rectangle: - if (this._inkToTextStartX && this._inkToTextStartY) { - const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); - const setDocs = this.getActiveDocuments().filter(s => DocCast(s.proto)?.type === DocumentType.RTF && s.color); - const sets = setDocs.map(sd => Cast(sd.text, RichTextField)?.Text as string); - if (sets.length && sets[0]) { - this._wordPalette.clear(); - const colors = setDocs.map(sd => FieldValue(sd.color) as string); - sets.forEach((st: string, i: number) => st.split(',').forEach(word => this._wordPalette.set(word, colors[i]))); - } - const inks = this.getActiveDocuments().filter(doc => { - if (doc.type === 'ink') { - const l = NumCast(doc.x); - const r = l + doc[Width](); - const t = NumCast(doc.y); - const b = t + doc[Height](); - const pass = !(this._inkToTextStartX! > r || end[0] < l || this._inkToTextStartY! > b || end[1] < t); - return pass; - } - return false; - }); - // const inkFields = inks.map(i => Cast(i.data, InkField)); - const strokes: InkData[] = []; - inks.forEach(i => { - const d = Cast(i.data, InkField); - const x = NumCast(i.x); - const y = NumCast(i.y); - const left = Math.min(...(d?.inkData.map(pd => pd.X) ?? [0])); - const top = Math.min(...(d?.inkData.map(pd => pd.Y) ?? [0])); - if (d) { - strokes.push(d.inkData.map(pd => ({ X: pd.X + x - left, Y: pd.Y + y - top }))); - } + 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 => { - const wordResults = results.filter((r: any) => r.category === 'inkWord'); - for (const word of wordResults) { - const indices: number[] = word.strokeIds; - indices.forEach(i => { - const otherInks: Doc[] = []; - indices.forEach(i2 => i2 !== i && otherInks.push(inks[i2])); - inks[i].relatedInks = new List<Doc>(otherInks); - const uniqueColors: string[] = []; - Array.from(this._wordPalette.values()).forEach(c => uniqueColors.indexOf(c) === -1 && uniqueColors.push(c)); - inks[i].alternativeColors = new List<string>(uniqueColors); - if (this._wordPalette.has(word.recognizedText.toLowerCase())) { - inks[i].color = this._wordPalette.get(word.recognizedText.toLowerCase()); - } else if (word.alternates) { - for (const alt of word.alternates) { - if (this._wordPalette.has(alt.recognizedString.toLowerCase())) { - inks[i].color = this._wordPalette.get(alt.recognizedString.toLowerCase()); - break; - } - } - } - }); - } - }); - this._inkToTextStartX = end[0]; - } + CognitiveServices.Inking.Appliers.InterpretStrokes(strokes).then(results => {}); break; case GestureUtils.Gestures.Text: if (ge.text) { - const B = this.getTransform().transformPoint(ge.points[0].X, ge.points[0].Y); + const B = this.screenToFreeformContentsXf.transformPoint(ge.points[0].X, ge.points[0].Y); this.addDocument(Docs.Create.TextDocument(ge.text, { title: ge.text, x: B[0], y: B[1] })); e.stopPropagation(); } @@ -803,7 +702,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @action onEraserUp = (e: PointerEvent): void => { - this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc)); + this._deleteList.forEach(ink => ink._props.removeDocument?.(ink.Document)); this._deleteList = []; this._batch?.end(); }; @@ -813,12 +712,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (this._lightboxDoc) this._lightboxDoc = undefined; if (Utils.isClick(e.pageX, e.pageY, this._downX, this._downY, this._downTime)) { if (this.onBrowseClickHandler()) { - this.onBrowseClickHandler().script.run({ documentView: this.props.DocumentView?.(), clientX: e.clientX, clientY: e.clientY }); + this.onBrowseClickHandler().script.run({ documentView: this.DocumentView?.(), clientX: e.clientX, clientY: e.clientY }); e.stopPropagation(); e.preventDefault(); } else if (this.isContentActive() && e.shiftKey) { // reset zoom of freeform view to 1-to-1 on a shift + double click - this.zoomSmoothlyAboutPt(this.getTransform().transformPoint(e.clientX, e.clientY), 1); + this.zoomSmoothlyAboutPt(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY), 1); e.stopPropagation(); e.preventDefault(); } @@ -832,11 +731,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @action - pan = (e: PointerEvent | React.Touch | { clientX: number; clientY: number }): void => { + pan = (e: PointerEvent): void => { + const ctrlKey = e.ctrlKey && !e.shiftKey; + const shiftKey = e.shiftKey && !e.ctrlKey; PresBox.Instance?.pauseAutoPres(); - this.props.DocumentView?.().clearViewTransition(); - const [dx, dy] = this.getTransform().transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); - this.setPan(NumCast(this.Document[this.panXFieldKey]) - dx, NumCast(this.Document[this.panYFieldKey]) - dy, 0, true); + this.DocumentView?.().clearViewTransition(); + const [dxi, dyi] = this.screenToFreeformContentsXf.transformDirection(e.clientX - this._lastX, e.clientY - this._lastY); + const { x: dx, y: dy } = Utils.rotPt(dxi, dyi, this.ScreenToLocalBoxXf().Rotate); + this.setPan(NumCast(this.Document[this.panXFieldKey]) - (ctrlKey ? 0 : dx), NumCast(this.Document[this.panYFieldKey]) - (shiftKey ? 0 : dy), 0, true); this._lastX = e.clientX; this._lastY = e.clientY; }; @@ -855,8 +757,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => { if (!this._deleteList.includes(intersect.inkView)) { this._deleteList.push(intersect.inkView); - SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.stroke_width?.toString()) || '1'); - SetActiveInkColor(StrCast(intersect.inkView.rootDoc.color?.toString()) || 'black'); + SetActiveInkWidth(StrCast(intersect.inkView.Document.stroke_width?.toString()) || '1'); + SetActiveInkColor(StrCast(intersect.inkView.Document.color?.toString()) || 'black'); // create a new curve by appending all curves of the current segment together in order to render a single new stroke. if (!e.shiftKey) { this._eraserLock++; @@ -882,20 +784,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @action - onPointerMove = (e: PointerEvent): boolean => { - if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return false; - if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { - Doc.ActiveTool = InkTool.None; - } else { - if (this.tryDragCluster(e, this._hitCluster)) { - 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; - } - // pan the view if this is a regular collection, or it's an overlay and the overlay is zoomed (otherwise, there's nothing to pan) - if (!this.props.isAnnotationOverlay || 1 - NumCast(this.rootDoc._freeform_scale_min, 1) / this.getLocalTransform().inverse().Scale) { - this.pan(e); - e.stopPropagation(); // if we are actually panning, stop propagation -- this will preven things like the overlayView from dragging the document while we're panning - } + onPointerMove = (e: PointerEvent) => { + if (this.tryDragCluster(e, this._hitCluster)) { + 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; + } + // pan the view if this is a regular collection, or it's an overlay and the overlay is zoomed (otherwise, there's nothing to pan) + if (!this._props.isAnnotationOverlay || 1 - NumCast(this.layoutDoc._freeform_scale_min, 1) / this.zoomScaling()) { + this.pan(e); + e.stopPropagation(); // if we are actually panning, stop propagation -- this will preven things like the overlayView from dragging the document while we're panning } return false; }; @@ -909,9 +806,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) }; return this.childDocs - .map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.DocumentView?.())) + .map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())) .filter(inkView => inkView?.ComponentView instanceof InkingStroke) - .map(inkView => ({ inkViewBounds: inkView!.getBounds(), inkStroke: inkView!.ComponentView as InkingStroke, inkView: inkView! })) + .map(inkView => ({ inkViewBounds: inkView!.getBounds, inkStroke: inkView!.ComponentView as InkingStroke, inkView: inkView! })) .filter( ({ inkViewBounds }) => inkViewBounds && // bounding box of eraser segment and ink stroke overlap @@ -1003,14 +900,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.childDocs .filter(doc => doc.type === DocumentType.INK && !doc.dontIntersect) .forEach(doc => { - const otherInk = DocumentManager.Instance.getDocumentView(doc, this.props.DocumentView?.())?.ComponentView as InkingStroke; + const otherInk = DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())?.ComponentView as InkingStroke; const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] }; const otherScreenPts = otherInkData.map(point => otherInk.ptToScreen(point)); const otherCtrlPts = otherScreenPts.map(spt => (ink.ComponentView as InkingStroke).ptFromScreen(spt)); for (var j = 0; j < otherCtrlPts.length - 3; j += 4) { const neighboringSegment = i === j || i === j - 4 || i === j + 4; // Ensuring that the curve intersected by the eraser is not checked for further ink intersections. - if (ink?.Document === otherInk.props.Document && neighboringSegment) continue; + if (ink?.Document === otherInk.Document && neighboringSegment) continue; const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y }))); const c0 = otherCurve.get(0); @@ -1033,60 +930,61 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return tVals; }; - cleanUpInteractions = () => { - this.removeMoveListeners(); - this.removeEndListeners(); - }; - @action zoom = (pointX: number, pointY: number, deltaY: number): void => { - if (this.Document._isGroup || this.Document[(this.props.viewField ?? '_') + 'freeform_noZoom']) return; + if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return; let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05; if (deltaScale < 0) deltaScale = -deltaScale; - const [x, y] = this.getTransform().transformPoint(pointX, pointY); - const invTransform = this.getLocalTransform().inverse(); + const [x, y] = this.screenToFreeformContentsXf.transformPoint(pointX, pointY); + const invTransform = this.panZoomXf.inverse(); if (deltaScale * invTransform.Scale > 20) { deltaScale = 20 / invTransform.Scale; } - if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc[this.scaleFieldKey + '_min'])) { + if (deltaScale < 1 && invTransform.Scale <= NumCast(this.Document[this.scaleFieldKey + '_min'])) { this.setPan(0, 0); return; } - if (deltaScale * invTransform.Scale > NumCast(this.rootDoc[this.scaleFieldKey + '_max'], Number.MAX_VALUE)) { - deltaScale = NumCast(this.rootDoc[this.scaleFieldKey + '_max'], 1) / invTransform.Scale; + if (deltaScale * invTransform.Scale > NumCast(this.Document[this.scaleFieldKey + '_max'], Number.MAX_VALUE)) { + deltaScale = NumCast(this.Document[this.scaleFieldKey + '_max'], 1) / invTransform.Scale; } - if (deltaScale * invTransform.Scale < NumCast(this.rootDoc[this.scaleFieldKey + '_min'], this.isAnnotationOverlay ? 1 : 0)) { - deltaScale = NumCast(this.rootDoc[this.scaleFieldKey + '_min'], 1) / invTransform.Scale; + if (deltaScale * invTransform.Scale < NumCast(this.Document[this.scaleFieldKey + '_min'], this.isAnnotationOverlay ? 1 : 0)) { + deltaScale = NumCast(this.Document[this.scaleFieldKey + '_min'], 1) / invTransform.Scale; } const localTransform = invTransform.scaleAbout(deltaScale, x, y); if (localTransform.Scale >= 0.05 || localTransform.Scale > this.zoomScaling()) { const safeScale = Math.min(Math.max(0.05, localTransform.Scale), 20); - this.props.Document[this.scaleFieldKey] = Math.abs(safeScale); - this.setPan(-localTransform.TranslateX / safeScale, (this.props.originTopLeft ? undefined : NumCast(this.props.Document.layout_scrollTop) * safeScale) || -localTransform.TranslateY / safeScale); + this.Document[this.scaleFieldKey] = Math.abs(safeScale); + this.setPan(-localTransform.TranslateX / safeScale, (this._props.originTopLeft ? undefined : NumCast(this.Document.layout_scrollTop) * safeScale) || -localTransform.TranslateY / safeScale); } }; @action onPointerWheel = (e: React.WheelEvent): void => { - if (this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom + if (this.Document.isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom PresBox.Instance?.pauseAutoPres(); - if (this.layoutDoc._Transform || this.props.Document.treeView_OutlineMode === TreeViewType.outline) return; + if (this.layoutDoc._Transform || this.Document.treeView_OutlineMode === TreeViewType.outline) return; e.stopPropagation(); - const docHeight = NumCast(this.rootDoc[Doc.LayoutFieldKey(this.rootDoc) + '_nativeHeight'], this.nativeHeight); - const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this.props.PanelHeight() / this.nativeDimScaling + 1e-4; - switch (!e.ctrlKey ? Doc.UserDoc().freeformScrollMode : freeformScrollMode.Pan) { + const docHeight = NumCast(this.Document[Doc.LayoutFieldKey(this.Document) + '_nativeHeight'], this.nativeHeight); + const scrollable = this.isAnnotationOverlay && NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this._props.PanelHeight() / this.nativeDimScaling + 1e-4; + switch ( + !e.ctrlKey && !e.shiftKey && !e.metaKey && !e.altKey ?// + Doc.UserDoc().freeformScrollMode : // no modifiers, do assigned mode + e.ctrlKey && !CtrlKey? // otherwise, if ctrl key (pinch gesture) try to zoom else pan + freeformScrollMode.Zoom : freeformScrollMode.Pan // prettier-ignore + ) { case freeformScrollMode.Pan: - // if ctrl is selected then zoom - if (!e.ctrlKey && this.props.isContentActive(true)) { - this.scrollPan({ deltaX: -e.deltaX * this.getTransform().Scale, deltaY: e.shiftKey ? 0 : -e.deltaY * this.getTransform().Scale }); + if (((!e.metaKey && !e.altKey) || Doc.UserDoc().freeformScrollMode === freeformScrollMode.Zoom) && this._props.isContentActive()) { + const deltaX = e.shiftKey ? e.deltaX : e.ctrlKey ? 0 : e.deltaX; + const deltaY = e.shiftKey ? 0 : e.ctrlKey ? e.deltaY : e.deltaY; + this.scrollPan({ deltaX: -deltaX * this.screenToFreeformContentsXf.Scale, deltaY: e.shiftKey ? 0 : -deltaY * this.screenToFreeformContentsXf.Scale }); break; } default: case freeformScrollMode.Zoom: - if ((e.ctrlKey || !scrollable) && this.props.isContentActive(true)) { - this.zoom(e.clientX, e.clientY, Math.max(-1, Math.min(1, e.deltaY))); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc? - e.preventDefault(); + if ((e.ctrlKey || !scrollable) && this._props.isContentActive()) { + this.zoom(e.clientX, e.clientY, Math.max(-1, Math.min(1, e.deltaY))); // if (!this._props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc? + // e.preventDefault(); } break; } @@ -1101,7 +999,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // this section wraps the pan position, horizontally and/or vertically whenever the content is panned out of the viewing bounds const docs = this.childLayoutPairs.map(pair => pair.layout).filter(doc => doc instanceof Doc); const measuredDocs = docs - .map(doc => ({ pos: this.childPositionProviderUnmemoized(doc, ''), size: this.childSizeProviderUnmemoized(doc, '') })) + .map(doc => ({ pos: { x: NumCast(doc.x), y: NumCast(doc.y) }, size: { width: NumCast(doc._width), height: NumCast(doc._height) } })) .filter(({ pos, size }) => pos && size) .map(({ pos, size }) => ({ pos: pos!, size: size! })); if (measuredDocs.length) { @@ -1114,45 +1012,45 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) }, }), { - xrange: { min: this.props.originTopLeft ? 0 : Number.MAX_VALUE, max: -Number.MAX_VALUE }, - yrange: { min: this.props.originTopLeft ? 0 : Number.MAX_VALUE, max: -Number.MAX_VALUE }, + xrange: { min: this._props.originTopLeft ? 0 : Number.MAX_VALUE, max: -Number.MAX_VALUE }, + yrange: { min: this._props.originTopLeft ? 0 : Number.MAX_VALUE, max: -Number.MAX_VALUE }, } ); - - const panelWidMax = (this.props.PanelWidth() / this.zoomScaling()) * (this.props.originTopLeft ? 2 / this.nativeDimScaling : 1); - const panelWidMin = (this.props.PanelWidth() / this.zoomScaling()) * (this.props.originTopLeft ? 0 : 1); - const panelHgtMax = (this.props.PanelHeight() / this.zoomScaling()) * (this.props.originTopLeft ? 2 / this.nativeDimScaling : 1); - const panelHgtMin = (this.props.PanelHeight() / this.zoomScaling()) * (this.props.originTopLeft ? 0 : 1); - if (ranges.xrange.min >= panX + panelWidMax / 2) panX = ranges.xrange.max + (this.props.originTopLeft ? 0 : panelWidMax / 2); - else if (ranges.xrange.max <= panX - panelWidMin / 2) panX = ranges.xrange.min - (this.props.originTopLeft ? panelWidMax / 2 : panelWidMin / 2); - if (ranges.yrange.min >= panY + panelHgtMax / 2) panY = ranges.yrange.max + (this.props.originTopLeft ? 0 : panelHgtMax / 2); - else if (ranges.yrange.max <= panY - panelHgtMin / 2) panY = ranges.yrange.min - (this.props.originTopLeft ? panelHgtMax / 2 : panelHgtMin / 2); + const scaling = this.zoomScaling() * (this._props.NativeDimScaling?.() || 1); + const panelWidMax = (this._props.PanelWidth() / scaling) * (this._props.originTopLeft ? 2 / this.nativeDimScaling : 1); + const panelWidMin = (this._props.PanelWidth() / scaling) * (this._props.originTopLeft ? 0 : 1); + const panelHgtMax = (this._props.PanelHeight() / scaling) * (this._props.originTopLeft ? 2 / this.nativeDimScaling : 1); + const panelHgtMin = (this._props.PanelHeight() / scaling) * (this._props.originTopLeft ? 0 : 1); + if (ranges.xrange.min >= panX + panelWidMax / 2) panX = ranges.xrange.max + (this._props.originTopLeft ? 0 : panelWidMax / 2); + else if (ranges.xrange.max <= panX - panelWidMin / 2) panX = ranges.xrange.min - (this._props.originTopLeft ? panelWidMax / 2 : panelWidMin / 2); + if (ranges.yrange.min >= panY + panelHgtMax / 2) panY = ranges.yrange.max + (this._props.originTopLeft ? 0 : panelHgtMax / 2); + else if (ranges.yrange.max <= panY - panelHgtMin / 2) panY = ranges.yrange.min - (this._props.originTopLeft ? panelHgtMax / 2 : panelHgtMin / 2); } } if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc) { this.setPanZoomTransition(panTime); - const minScale = NumCast(this.rootDoc._freeform_scale_min, 1); - const scale = 1 - minScale / this.getLocalTransform().inverse().Scale; - const minPanX = NumCast(this.rootDoc._freeform_panX_min, 0); - const minPanY = NumCast(this.rootDoc._freeform_panY_min, 0); - const maxPanX = NumCast(this.rootDoc._freeform_panX_max, this.nativeWidth); + const minScale = NumCast(this.dataDoc._freeform_scale_min, 1); + const scale = 1 - minScale / this.zoomScaling(); + const minPanX = NumCast(this.dataDoc._freeform_panX_min, 0); + const minPanY = NumCast(this.dataDoc._freeform_panY_min, 0); + const maxPanX = NumCast(this.dataDoc._freeform_panX_max, this.nativeWidth); const newPanX = Math.min(minPanX + scale * maxPanX, Math.max(minPanX, panX)); - const fitYscroll = (((this.nativeHeight / this.nativeWidth) * this.props.PanelWidth() - this.props.PanelHeight()) * this.props.ScreenToLocalTransform().Scale) / minScale; - const nativeHeight = (this.props.PanelHeight() / this.props.PanelWidth() / (this.nativeHeight / this.nativeWidth)) * this.nativeHeight; - const maxScrollTop = this.nativeHeight / this.props.ScreenToLocalTransform().Scale - this.props.PanelHeight(); + const fitYscroll = (((this.nativeHeight / this.nativeWidth) * this._props.PanelWidth() - this._props.PanelHeight()) * this.ScreenToLocalBoxXf().Scale) / minScale; + const nativeHeight = (this._props.PanelHeight() / this._props.PanelWidth() / (this.nativeHeight / this.nativeWidth)) * this.nativeHeight; + const maxScrollTop = this.nativeHeight / this.ScreenToLocalBoxXf().Scale - this._props.PanelHeight(); const maxPanY = minPanY + // minPanY + scrolling introduced by view scaling + scrolling introduced by layout_fitWidth - scale * NumCast(this.rootDoc._panY_max, nativeHeight) + - (!this.props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning + scale * NumCast(this.dataDoc._panY_max, nativeHeight) + + (!this._props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning let newPanY = Math.max(minPanY, Math.min(maxPanY, panY)); - if (false && NumCast(this.rootDoc.layout_scrollTop) && NumCast(this.rootDoc._freeform_scale, minScale) !== minScale) { - const relTop = NumCast(this.rootDoc.layout_scrollTop) / maxScrollTop; - this.rootDoc.layout_scrollTop = undefined; + if (false && NumCast(this.layoutDoc.layout_scrollTop) && NumCast(this.layoutDoc._freeform_scale, minScale) !== minScale) { + const relTop = NumCast(this.layoutDoc.layout_scrollTop) / maxScrollTop; + this.layoutDoc.layout_scrollTop = undefined; newPanY = minPanY + relTop * (maxPanY - minPanY); - } else if (fitYscroll > 2 && this.rootDoc.layout_scrollTop === undefined && NumCast(this.rootDoc._freeform_scale, minScale) === minScale) { + } else if (fitYscroll > 2 && this.layoutDoc.layout_scrollTop === undefined && NumCast(this.layoutDoc._freeform_scale, minScale) === minScale) { const maxPanY = minPanY + fitYscroll; const relTop = (panY - minPanY) / (maxPanY - minPanY); - setTimeout(() => (this.rootDoc.layout_scrollTop = relTop * maxScrollTop), 10); + setTimeout(() => (this.layoutDoc.layout_scrollTop = relTop * maxScrollTop), 10); newPanY = minPanY; } !this.Document._verticalScroll && (this.Document[this.panXFieldKey] = this.isAnnotationOverlay ? newPanX : panX); @@ -1162,11 +1060,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action nudge = (x: number, y: number, nudgeTime: number = 500) => { - const collectionDoc = this.props.docViewPath().lastElement().rootDoc; - if (collectionDoc?._type_collection !== CollectionViewType.Freeform || collectionDoc._freeform_ !== undefined) { + const collectionDoc = this.Document; + if (collectionDoc?._type_collection !== CollectionViewType.Freeform) { this.setPan( - NumCast(this.layoutDoc[this.panXFieldKey]) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale - NumCast(this.layoutDoc[this.panYFieldKey]) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(), + NumCast(this.layoutDoc[this.panXFieldKey]) + ((this._props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale + NumCast(this.layoutDoc[this.panYFieldKey]) + ((this._props.PanelHeight() / 2) * -y) / this.zoomScaling(), nudgeTime, true ); @@ -1211,13 +1109,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action zoomSmoothlyAboutPt(docpt: number[], scale: number, transitionTime = 500) { - if (this.Document._isGroup) return; + if (this.Document.isGroup) return; this.setPanZoomTransition(transitionTime); - const screenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); + const screenXY = this.screenToFreeformContentsXf.inverse().transformPoint(docpt[0], docpt[1]); this.layoutDoc[this.scaleFieldKey] = scale; - const newScreenXY = this.getTransform().inverse().transformPoint(docpt[0], docpt[1]); + const newScreenXY = this.screenToFreeformContentsXf.inverse().transformPoint(docpt[0], docpt[1]); const scrDelta = { x: screenXY[0] - newScreenXY[0], y: screenXY[1] - newScreenXY[1] }; - const newpan = this.getTransform().transformDirection(scrDelta.x, scrDelta.y); + const newpan = this.screenToFreeformContentsXf.transformDirection(scrDelta.x, scrDelta.y); this.layoutDoc[this.panXFieldKey] = NumCast(this.layoutDoc[this.panXFieldKey]) - newpan[0]; this.layoutDoc[this.panYFieldKey] = NumCast(this.layoutDoc[this.panYFieldKey]) - newpan[1]; } @@ -1225,7 +1123,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => { const layoutdoc = Doc.Layout(doc); const pt = xf.transformPoint(NumCast(doc.x), NumCast(doc.y)); - const pt2 = xf.transformPoint(NumCast(doc.x) + layoutdoc[Width](), NumCast(doc.y) + layoutdoc[Height]()); + const pt2 = xf.transformPoint(NumCast(doc.x) + NumCast(layoutdoc._width), NumCast(doc.y) + NumCast(layoutdoc._height)); const bounds = { left: pt[0], right: pt2[0], top: pt[1], bot: pt2[1], width: pt2[0] - pt[0], height: pt2[1] - pt[1] }; if (scale !== undefined) { @@ -1233,20 +1131,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const newScale = scale === 0 ? NumCast(this.layoutDoc[this.scaleFieldKey]) - : Math.min(maxZoom, (1 / (this.nativeDimScaling || 1)) * scale * Math.min(this.props.PanelWidth() / Math.abs(bounds.width), this.props.PanelHeight() / Math.abs(bounds.height))); + : Math.min(maxZoom, (1 / (this.nativeDimScaling || 1)) * scale * Math.min(this._props.PanelWidth() / Math.abs(bounds.width), this._props.PanelHeight() / Math.abs(bounds.height))); return { - panX: this.props.isAnnotationOverlay ? bounds.left - (Doc.NativeWidth(this.layoutDoc) / newScale - bounds.width) / 2 : (bounds.left + bounds.right) / 2, - panY: this.props.isAnnotationOverlay ? bounds.top - (Doc.NativeHeight(this.layoutDoc) / newScale - bounds.height) / 2 : (bounds.top + bounds.bot) / 2, + panX: this._props.isAnnotationOverlay ? bounds.left - (Doc.NativeWidth(this.layoutDoc) / newScale - bounds.width) / 2 : (bounds.left + bounds.right) / 2, + panY: this._props.isAnnotationOverlay ? bounds.top - (Doc.NativeHeight(this.layoutDoc) / newScale - bounds.height) / 2 : (bounds.top + bounds.bot) / 2, scale: newScale, }; } - const panelWidth = this.props.isAnnotationOverlay ? this.nativeWidth : this.props.PanelWidth(); - const panelHeight = this.props.isAnnotationOverlay ? this.nativeHeight : this.props.PanelHeight(); + const panelWidth = this._props.isAnnotationOverlay ? this.nativeWidth : this._props.PanelWidth(); + const panelHeight = this._props.isAnnotationOverlay ? this.nativeHeight : this._props.PanelHeight(); const pw = panelWidth / NumCast(this.layoutDoc._freeform_scale, 1); const ph = panelHeight / NumCast(this.layoutDoc._freeform_scale, 1); - const cx = NumCast(this.layoutDoc[this.panXFieldKey]) + (this.props.isAnnotationOverlay ? pw / 2 : 0); - const cy = NumCast(this.layoutDoc[this.panYFieldKey]) + (this.props.isAnnotationOverlay ? ph / 2 : 0); + const cx = NumCast(this.layoutDoc[this.panXFieldKey]) + (this._props.isAnnotationOverlay ? pw / 2 : 0); + const cy = NumCast(this.layoutDoc[this.panYFieldKey]) + (this._props.isAnnotationOverlay ? ph / 2 : 0); const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 }; const maxYShift = Math.max(0, screen.bot - screen.top - (bounds.bot - bounds.top)); const phborder = bounds.top < screen.top || bounds.bot > screen.bot ? Math.min(ph / 10, maxYShift / 2) : 0; @@ -1254,115 +1152,117 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return { panX: (bounds.left + bounds.right) / 2, panY: (bounds.top + bounds.bot) / 2, - scale: Math.min(this.props.PanelHeight() / (bounds.bot - bounds.top), this.props.PanelWidth() / (bounds.right - bounds.left)) / 1.1, + scale: Math.min(this._props.PanelHeight() / (bounds.bot - bounds.top), this._props.PanelWidth() / (bounds.right - bounds.left)) / 1.1, }; } return { - panX: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc[this.panXFieldKey]) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right), - panY: (this.props.isAnnotationOverlay ? NumCast(this.layoutDoc[this.panYFieldKey]) : cy) + Math.min(0, bounds.top - phborder - screen.top) + Math.max(0, bounds.bot + phborder - screen.bot), + panX: (this._props.isAnnotationOverlay ? NumCast(this.layoutDoc[this.panXFieldKey]) : cx) + Math.min(0, bounds.left - pw / 10 - screen.left) + Math.max(0, bounds.right + pw / 10 - screen.right), + panY: (this._props.isAnnotationOverlay ? NumCast(this.layoutDoc[this.panYFieldKey]) : cy) + Math.min(0, bounds.top - phborder - screen.top) + Math.max(0, bounds.bot + phborder - screen.bot), }; }; - isContentActive = () => this.props.isContentActive(); + isContentActive = () => this._props.isContentActive(); @undoBatch - @action onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => { - const docView = fieldProps.DocumentView?.(); - if (docView && (e.metaKey || e.ctrlKey || e.altKey || docView.rootDoc._createDocOnCR) && ['Tab', 'Enter'].includes(e.key)) { + if ((e.metaKey || e.ctrlKey || e.altKey || fieldProps.Document._createDocOnCR) && ['Tab', 'Enter'].includes(e.key)) { e.stopPropagation?.(); const below = !e.altKey && e.key !== 'Tab'; - const layout_fieldKey = StrCast(docView.LayoutFieldKey); - const newDoc = Doc.MakeCopy(docView.rootDoc, true); - const dataField = docView.rootDoc[Doc.LayoutFieldKey(newDoc)]; + const layout_fieldKey = StrCast(fieldProps.fieldKey); + const newDoc = Doc.MakeCopy(fieldProps.Document, true); + const dataField = fieldProps.Document[Doc.LayoutFieldKey(newDoc)]; newDoc[DocData][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; - if (below) newDoc.y = NumCast(docView.rootDoc.y) + NumCast(docView.rootDoc._height) + 10; - else newDoc.x = NumCast(docView.rootDoc.x) + NumCast(docView.rootDoc._width) + 10; - if (layout_fieldKey !== 'layout' && docView.rootDoc[layout_fieldKey] instanceof Doc) { - newDoc[layout_fieldKey] = docView.rootDoc[layout_fieldKey]; + if (below) newDoc.y = NumCast(fieldProps.Document.y) + NumCast(fieldProps.Document._height) + 10; + else newDoc.x = NumCast(fieldProps.Document.x) + NumCast(fieldProps.Document._width) + 10; + if (layout_fieldKey !== 'layout' && fieldProps.Document[layout_fieldKey] instanceof Doc) { + newDoc[layout_fieldKey] = fieldProps.Document[layout_fieldKey]; } - Doc.GetProto(newDoc).text = undefined; - FormattedTextBox.SelectOnLoad = newDoc[Id]; + newDoc[DocData].text = undefined; + FormattedTextBox.SetSelectOnLoad(newDoc); return this.addDocument?.(newDoc); } }; @computed get childPointerEvents() { - const engine = this.props.layoutEngine?.() || StrCast(this.props.Document._layoutEngine); - const pointerEvents = DocumentView.Interacting + 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(true)) || this.isContentActive() === false ? 'none' : this.props.pointerEvents?.()); - return pointerEvents; + : this._props.childPointerEvents?.() ?? + (this._props.viewDefDivClick || // + (engine === computePassLayout.name && !this._props.isSelected()) || + this.isContentActive() === false + ? 'none' + : this._props.pointerEvents?.()); } - childPointerEventsFunc = () => this.childPointerEvents; - childContentsActive = () => (this.props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction)(); + + @observable _childPointerEvents: 'none' | 'all' | 'visiblepainted' | undefined = undefined; + childPointerEventsFunc = () => this._childPointerEvents; + childContentsActive = () => (this._props.childContentsActive ?? this.isContentActive() === false ? returnFalse : emptyFunction)(); getChildDocView(entry: PoolData) { const childLayout = entry.pair.layout; const childData = entry.pair.data; return ( - <CollectionFreeFormDocumentView + <CollectionFreeFormDocumentViewWrapper + {...OmitKeys(entry, ['replica', 'pair']).omit} key={childLayout[Id] + (entry.replica || '')} - DataDoc={childData} Document={childLayout} - isGroupActive={this.props.isGroupActive} - renderDepth={this.props.renderDepth + 1} - replica={entry.replica} + containerViewPath={this.DocumentView?.().docViewPath} + styleProvider={this.clusterStyleProvider} + TemplateDataDocument={childData} + dragStarting={this.dragStarting} + dragEnding={this.dragEnding} + isGroupActive={this._props.isGroupActive} + renderDepth={this._props.renderDepth + 1} hideDecorations={BoolCast(childLayout._layout_isSvg && childLayout.type === DocumentType.LINK)} suppressSetHeight={this.layoutEngine ? true : false} - renderCutoffProvider={this.renderCutoffProvider} + RenderCutoffProvider={this.renderCutoffProvider} CollectionFreeFormView={this} - LayoutTemplate={childLayout.z ? undefined : this.props.childLayoutTemplate} - LayoutTemplateString={childLayout.z ? undefined : this.props.childLayoutString} + LayoutTemplate={childLayout.z ? undefined : this._props.childLayoutTemplate} + LayoutTemplateString={childLayout.z ? undefined : this._props.childLayoutString} rootSelected={childData ? this.rootSelected : returnFalse} - waitForDoubleClickToClick={this.props.waitForDoubleClickToClick} - onClick={this.onChildClickHandler} + waitForDoubleClickToClick={this._props.waitForDoubleClickToClick} + onClickScript={this.onChildClickHandler} onKey={this.onKeyDown} - onDoubleClick={this.onChildDoubleClickHandler} - onBrowseClick={this.onBrowseClickHandler} - ScreenToLocalTransform={childLayout.z ? this.getContainerTransform : this.getTransform} + onDoubleClickScript={this.onChildDoubleClickHandler} + onBrowseClickScript={this.onBrowseClickHandler} + ScreenToLocalTransform={childLayout.z ? this.ScreenToLocalBoxXf : this.ScreenToContentsXf} PanelWidth={childLayout[Width]} PanelHeight={childLayout[Height]} childFilters={this.childDocFilters} childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} - isDocumentActive={childLayout.pointerEvents === 'none' ? returnFalse : this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive} + isDocumentActive={childLayout.pointerEvents === 'none' ? returnFalse : this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this.isContentActive} isContentActive={this.childContentsActive} - focus={this.Document._isGroup ? this.groupFocus : this.isAnnotationOverlay ? this.props.focus : this.focus} + focus={this.Document.isGroup ? this.groupFocus : this.isAnnotationOverlay ? this._props.focus : this.focus} addDocTab={this.addDocTab} - addDocument={this.props.addDocument} - removeDocument={this.props.removeDocument} - moveDocument={this.props.moveDocument} - pinToPres={this.props.pinToPres} - whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} - docViewPath={this.props.docViewPath} - styleProvider={this.getClusterColor} - dragAction={(this.rootDoc.childDragAction ?? this.props.childDragAction) as dropActionType} - dataProvider={this.childDataProvider} - sizeProvider={this.childSizeProvider} - bringToFront={this.bringToFront} - layout_showTitle={this.props.childlayout_showTitle} - dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView} + addDocument={this._props.addDocument} + removeDocument={this._props.removeDocument} + moveDocument={this._props.moveDocument} + pinToPres={this._props.pinToPres} + whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} + dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType} + layout_showTitle={this._props.childlayout_showTitle} + dontRegisterView={this._props.dontRegisterView} pointerEvents={this.childPointerEventsFunc} - //fitContentsToBox={this.props.fitContentsToBox || BoolCast(this.props.treeView_FreezeChildDimensions)} // bcz: check this /> ); } addDocTab = action((doc: Doc, where: OpenWhere) => { - if (this.props.isAnnotationOverlay) return this.props.addDocTab(doc, where); + if (this._props.isAnnotationOverlay) return this._props.addDocTab(doc, where); switch (where) { case OpenWhere.inParent: - return this.props.addDocument?.(doc) || false; + return this._props.addDocument?.(doc) || false; case OpenWhere.inParentFromScreen: const docContext = DocCast((doc instanceof Doc ? doc : doc?.[0])?.embedContainer); return ( (this.addDocument?.( (doc instanceof Doc ? [doc] : doc).map(doc => { - const pt = this.getTransform().transformPoint(NumCast(doc.x), NumCast(doc.y)); + const pt = this.screenToFreeformContentsXf.transformPoint(NumCast(doc.x), NumCast(doc.y)); doc.x = pt[0]; doc.y = pt[1]; return doc; }) ) && - (!docContext || this.props.removeDocument?.(docContext))) || + (!docContext || this._props.removeDocument?.(docContext))) || false ); case undefined: @@ -1376,21 +1276,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return true; } } - return this.props.addDocTab(doc, where); + return this._props.addDocTab(doc, where); }); - @observable _lightboxDoc: Opt<Doc>; + @observable _lightboxDoc: Opt<Doc> = undefined; - getCalculatedPositions(params: { pair: { layout: Doc; data?: Doc }; index: number; collection: Doc }): PoolData { + 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); - const childDoc = params.pair.layout; + const childDoc = pair.layout; const childDocLayout = Doc.Layout(childDoc); const layoutFrameNumber = Cast(this.Document._currentFrame, 'number'); // frame number that container is at which determines layout frame values const contentFrameNumber = Cast(childDocLayout._currentFrame, 'number', layoutFrameNumber ?? null); // frame number that content is at which determines what content is displayed const { z, zIndex } = childDoc; const { backgroundColor, color } = contentFrameNumber === undefined ? { backgroundColor: undefined, color: undefined } : CollectionFreeFormDocumentView.getStringValues(childDoc, contentFrameNumber); - const { x, y, _width, _height, opacity, _rotation } = - layoutFrameNumber === undefined - ? { _width: Cast(childDocLayout._width, 'number'), _height: Cast(childDocLayout._height, 'number'), _rotation: Cast(childDocLayout._rotation, 'number'), x: childDoc.x, y: childDoc.y, opacity: this.props.childOpacity?.() } + const { x, y, autoDim, _width, _height, opacity, _rotation } = + layoutFrameNumber === undefined // -1 for width/height means width/height should be PanelWidth/PanelHeight (prevents collectionfreeformdocumentview width/height from getting out of synch with panelWIdth/Height which causes detailView to re-render and lose focus because HTMLtag scaling gets set to a bad intermediate value) + ? { autoDim: 1, _width: Cast(childDoc._width, 'number'), _height: Cast(childDoc._height, 'number'), _rotation: Cast(childDocLayout._rotation, 'number'), x: childDoc.x, y: childDoc.y, opacity: this._props.childOpacity?.() } : CollectionFreeFormDocumentView.getValues(childDoc, layoutFrameNumber); // prettier-ignore const rotation = Cast(_rotation,'number', @@ -1400,22 +1300,23 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection x: Number.isNaN(NumCast(x)) ? 0 : NumCast(x), y: Number.isNaN(NumCast(y)) ? 0 : NumCast(y), z: Cast(z, 'number'), - rotation: rotation, - color: Cast(color, 'string') ? StrCast(color) : this.props.styleProvider?.(childDoc, this.props, StyleProp.Color), - backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.getClusterColor(childDoc, this.props, StyleProp.BackgroundColor), - opacity: !_width ? 0 : this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this.props.styleProvider?.(childDoc, this.props, StyleProp.Opacity), + autoDim, + rotation, + color: Cast(color, 'string') ? StrCast(color) : this._props.styleProvider?.(childDoc, this._props, StyleProp.Color), + backgroundColor: Cast(backgroundColor, 'string') ? StrCast(backgroundColor) : this.clusterStyleProvider(childDoc, this._props, StyleProp.BackgroundColor), + opacity: !_width ? 0 : this._keyframeEditing ? 1 : Cast(opacity, 'number') ?? this._props.styleProvider?.(childDoc, this._props, StyleProp.Opacity), zIndex: Cast(zIndex, 'number'), width: _width, height: _height, transition: StrCast(childDocLayout.dataTransition), - pair: params.pair, pointerEvents: Cast(childDoc.pointerEvents, 'string', null), + pair, replica: '', }; } onViewDefDivClick = (e: React.MouseEvent, payload: any) => { - (this.props.viewDefDivClick || ScriptCast(this.props.Document.onViewDefDivClick))?.script.run({ this: this.props.Document, payload }); + (this._props.viewDefDivClick || ScriptCast(this.Document.onViewDefDivClick))?.script.run({ this: this.Document, payload }); e.stopPropagation(); }; @@ -1466,34 +1367,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }.bind(this) ); - childPositionProviderUnmemoized = (doc: Doc, replica: string) => this._layoutPoolData.get(doc[Id] + (replica || '')); - childDataProvider = computedFn( - function childDataProvider(this: any, doc: Doc, replica: string) { - return this.childPositionProviderUnmemoized(doc, replica); - }.bind(this) - ); - - childSizeProviderUnmemoized = (doc: Doc, replica: string) => this._layoutSizeData.get(doc[Id] + (replica || '')); - childSizeProvider = computedFn( - function childSizeProvider(this: any, doc: Doc, replica: string) { - return this.childSizeProviderUnmemoized(doc, replica); - }.bind(this) - ); - doEngineLayout( poolData: Map<string, PoolData>, engine: (poolData: Map<string, PoolData>, pivotDoc: Doc, childPairs: { layout: Doc; data?: Doc }[], panelDim: number[], viewDefsToJSX: (views: ViewDefBounds[]) => ViewDefResult[], engineProps: any) => ViewDefResult[] ) { - return engine(poolData, this.props.Document, this.childLayoutPairs, [this.props.PanelWidth(), this.props.PanelHeight()], this.viewDefsToJSX, this.props.engineProps); + return engine(poolData, this.Document, this.childLayoutPairs, [this._props.PanelWidth(), this._props.PanelHeight()], this.viewDefsToJSX, this._props.engineProps); } doFreeformLayout(poolData: Map<string, PoolData>) { - this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => poolData.set(pair.layout[Id], this.getCalculatedPositions({ pair, index: i, collection: this.Document }))); + this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map((pair, i) => poolData.set(pair.layout[Id], this.getCalculatedPositions(pair))); return [] as ViewDefResult[]; } @computed get layoutEngine() { - return this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine); + return this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine); } @computed get doInternalLayoutComputation() { TraceMobx(); @@ -1508,94 +1395,58 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return { newPool, computedElementData: this.doFreeformLayout(newPool) }; } - @observable _numLoaded = 1; - _lastPoolSize = 0; - get doLayoutComputation() { - const { newPool, computedElementData } = this.doInternalLayoutComputation; - const array = Array.from(newPool.entries()); - let somethingChanged = array.length !== this._lastPoolSize; - this._lastPoolSize = array.length; - runInAction(() => { - for (const entry of array) { - const lastPos = this._cachedPool.get(entry[0]); // last computed pos - const newPos = entry[1]; - if ( - !lastPos || - newPos.color !== lastPos.color || - newPos.backgroundColor !== lastPos.backgroundColor || - newPos.opacity !== lastPos.opacity || - newPos.x !== lastPos.x || - newPos.y !== lastPos.y || - newPos.z !== lastPos.z || - newPos.rotation !== lastPos.rotation || - newPos.zIndex !== lastPos.zIndex || - newPos.transition !== lastPos.transition || - newPos.pointerEvents !== lastPos.pointerEvents - ) { - this._layoutPoolData.set(entry[0], newPos); - somethingChanged = true; - } - if (!lastPos || newPos.height !== lastPos.height || newPos.width !== lastPos.width) { - this._layoutSizeData.set(entry[0], { width: newPos.width, height: newPos.height }); - somethingChanged = true; - } - } - }); - if (!somethingChanged) return undefined; - this._cachedPool.clear(); - Array.from(newPool.entries()).forEach(k => this._cachedPool.set(k[0], k[1])); + @action + doLayoutComputation = (newPool: Map<string, PoolData>, computedElementData: ViewDefResult[]) => { const elements = computedElementData.slice(); Array.from(newPool.entries()) .filter(entry => this.isCurrent(entry[1].pair.layout)) - .forEach((entry, i) => { - const childData: ViewDefBounds = this.childDataProvider(entry[1].pair.layout, entry[1].replica); - const childSize = this.childSizeProvider(entry[1].pair.layout, entry[1].replica); + .forEach(entry => elements.push({ ele: this.getChildDocView(entry[1]), - bounds: childData.opacity === 0 ? { ...childData, width: 0, height: 0 } : { ...childData, width: childSize.width, height: childSize.height }, + bounds: (entry[1].opacity === 0 ? { ...entry[1], width: 0, height: 0 } : { ...entry[1] }) as any, inkMask: BoolCast(entry[1].pair.layout.stroke_isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1, - }); - }); + }) + ); this.Document._freeform_useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); return elements; - } + }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { // create an anchor that saves information about the current state of the freeform view (pan, zoom, view type) - const anchor = Docs.Create.ConfigDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._type_collection), layout_unrendered: true, presentation_transition: 500, annotationOn: this.rootDoc }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document._isGroup, type_collection: true, filters: true } }, this.rootDoc); + const anchor = Docs.Create.ConfigDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._type_collection), layout_unrendered: true, presentation_transition: 500, annotationOn: this.Document }); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document.isGroup, type_collection: true, filters: true } }, this.Document); if (addAsAnnotation) { - if (Cast(this.dataDoc[this.props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) { - Cast(this.dataDoc[this.props.fieldKey + '_annotations'], listSpec(Doc), []).push(anchor); + if (Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) { + Cast(this.dataDoc[this._props.fieldKey + '_annotations'], listSpec(Doc), []).push(anchor); } else { - this.dataDoc[this.props.fieldKey + '_annotations'] = new List<Doc>([anchor]); + this.dataDoc[this._props.fieldKey + '_annotations'] = new List<Doc>([anchor]); } } return anchor; }; - @action + @action closeInfo = () => (this.Document._hideInfo = true); + infoUI = () => (this.Document._hideInfo || this.Document.annotationOn || this._props.renderDepth ? null : <CollectionFreeFormInfoUI Document={this.Document} Freeform={this} close={this.closeInfo} />); + componentDidMount() { - this.printDoc(this.rootDoc); - this.props.setContentView?.(this); + this._props.setContentViewBox?.(this); super.componentDidMount?.(); - this.props.setBrushViewer?.(this.brushView); setTimeout( action(() => { this._firstRender = false; this._disposers.groupBounds = reaction( () => { - if (this.Document._isGroup && this.childDocs.length === this.childDocList?.length) { - const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[Width](), height: cd[Height]() })); + if (this.Document.isGroup && this.childDocs.length === this.childDocList?.length) { + const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: NumCast(cd._width), height: NumCast(cd._height) })); return aggregateBounds(clist, NumCast(this.layoutDoc._xPadding), NumCast(this.layoutDoc._yPadding)); } return undefined; }, cbounds => { if (cbounds) { - const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[Width]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[Height]() / 2]; + const c = [NumCast(this.layoutDoc.x) + NumCast(this.layoutDoc._width) / 2, NumCast(this.layoutDoc.y) + NumCast(this.layoutDoc._height) / 2]; const p = [NumCast(this.layoutDoc[this.panXFieldKey]), NumCast(this.layoutDoc[this.panYFieldKey])]; const pbounds = { x: cbounds.x - p[0] + c[0], @@ -1616,20 +1467,24 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection { fireImmediately: true } ); - this._disposers.layoutComputation = reaction( - () => this.doLayoutComputation, - elements => { - if (elements !== undefined) this._layoutElements = elements || []; - }, - { fireImmediately: true, name: 'doLayout' } + this._disposers.pointerevents = reaction( + () => this.childPointerEvents, + pointerevents => (this._childPointerEvents = pointerevents as any), + { fireImmediately: true } ); this._disposers.active = reaction( () => this.isContentActive(), // if autoreset is on, then whenever the view is selected, it will be restored to it default pan/zoom positions - active => !SnappingManager.GetIsDragging() && this.rootDoc[this.autoResetFieldKey] && active && this.resetView() + active => !SnappingManager.IsDragging && this.dataDoc[this.autoResetFieldKey] && active && this.resetView() ); }) ); + this._disposers.layoutElements = reaction( + // layoutElements can't be a computed value because doLayoutComputation() is an action that has side effect of updating clusters + () => this.doInternalLayoutComputation, + computation => (this._layoutElements = this.doLayoutComputation(computation.newPool, computation.computedElementData)), + { fireImmediately: true } + ); } static replaceCanvases(oldDiv: HTMLElement, newDiv: HTMLElement) { @@ -1667,19 +1522,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection updateIcon = () => CollectionFreeFormView.UpdateIcon( this.layoutDoc[Id] + '-icon' + new Date().getTime(), - this.props.docViewPath().lastElement().ContentDiv!, - this.layoutDoc[Width](), - this.layoutDoc[Height](), - this.props.PanelWidth(), - this.props.PanelHeight(), + this.DocumentView?.().ContentDiv!, + NumCast(this.layoutDoc._width), + NumCast(this.layoutDoc._height), + this._props.PanelWidth(), + this._props.PanelHeight(), 0, 1, false, '', (iconFile, nativeWidth, nativeHeight) => { this.dataDoc.icon = new ImageField(iconFile); - this.dataDoc['icon_nativeWidth'] = nativeWidth; - this.dataDoc['icon_nativeHeight'] = nativeHeight; + this.dataDoc.icon_nativeWidth = nativeWidth; + this.dataDoc.icon_nativeHeight = nativeHeight; } ); @@ -1714,8 +1569,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } componentWillUnmount() { + this.dataDoc[this.autoResetFieldKey] && this.resetView(); Object.values(this._disposers).forEach(disposer => disposer?.()); - this._marqueeRef?.removeEventListener('dashDragAutoScroll', this.onDragAutoScroll as any); } @action @@ -1723,35 +1578,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); }; - @action - onDragAutoScroll = (e: CustomEvent<React.DragEvent>) => { - if ((e as any).handlePan || this.props.isAnnotationOverlay) return; - (e as any).handlePan = true; - - if (!this.layoutDoc._freeform_noAutoPan && !this.props.renderDepth && this._marqueeRef) { - const dragX = e.detail.clientX; - const dragY = e.detail.clientY; - const bounds = this._marqueeRef?.getBoundingClientRect(); - - const deltaX = dragX - bounds.left < 25 ? -(25 + (bounds.left - dragX)) : bounds.right - dragX < 25 ? 25 - (bounds.right - dragX) : 0; - const deltaY = dragY - bounds.top < 25 ? -(25 + (bounds.top - dragY)) : bounds.bottom - dragY < 25 ? 25 - (bounds.bottom - dragY) : 0; - if (deltaX !== 0 || deltaY !== 0) { - this.Document[this.panYFieldKey] = NumCast(this.Document[this.panYFieldKey]) + deltaY / 2; - this.Document[this.panXFieldKey] = NumCast(this.Document[this.panXFieldKey]) + deltaX / 2; - } - } - e.stopPropagation(); - }; - @undoBatch promoteCollection = () => { const childDocs = this.childDocs.slice(); childDocs.forEach(doc => { - const scr = this.getTransform().inverse().transformPoint(NumCast(doc.x), NumCast(doc.y)); + const scr = this.screenToFreeformContentsXf.inverse().transformPoint(NumCast(doc.x), NumCast(doc.y)); doc.x = scr?.[0]; doc.y = scr?.[1]; }); - this.props.addDocTab(childDocs as any as Doc, OpenWhere.inParentFromScreen); + this._props.addDocTab(childDocs as any as Doc, OpenWhere.inParentFromScreen); }; @undoBatch @@ -1775,9 +1610,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection /// @undoBatch resetView = () => { - this.rootDoc[this.panXFieldKey] = NumCast(this.rootDoc[this.panXFieldKey + '_reset']); - this.props.Document[this.panYFieldKey] = NumCast(this.rootDoc[this.panYFieldKey + '_reset']); - this.rootDoc[this.scaleFieldKey] = NumCast(this.rootDoc[this.scaleFieldKey + '_reset'], 1); + this.layoutDoc[this.panXFieldKey] = NumCast(this.dataDoc[this.panXFieldKey + '_reset']); + this.layoutDoc[this.panYFieldKey] = NumCast(this.dataDoc[this.panYFieldKey + '_reset']); + this.layoutDoc[this.scaleFieldKey] = NumCast(this.dataDoc[this.scaleFieldKey + '_reset'], 1); }; /// /// resetView restores a freeform collection to unit scale and centered at (0,0) UNLESS @@ -1785,11 +1620,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection /// @undoBatch toggleResetView = () => { - this.rootDoc[this.autoResetFieldKey] = !this.rootDoc[this.autoResetFieldKey]; - if (this.rootDoc[this.autoResetFieldKey]) { - this.rootDoc[this.panXFieldKey + '_reset'] = this.rootDoc[this.panXFieldKey]; - this.rootDoc[this.panYFieldKey + '_reset'] = this.rootDoc[this.panYFieldKey]; - this.rootDoc[this.scaleFieldKey + '_reset'] = this.rootDoc[this.scaleFieldKey]; + this.dataDoc[this.autoResetFieldKey] = !this.dataDoc[this.autoResetFieldKey]; + if (this.dataDoc[this.autoResetFieldKey]) { + this.dataDoc[this.panXFieldKey + '_reset'] = this.dataDoc[this.panXFieldKey]; + this.dataDoc[this.panYFieldKey + '_reset'] = this.dataDoc[this.panYFieldKey]; + this.dataDoc[this.scaleFieldKey + '_reset'] = this.dataDoc[this.scaleFieldKey]; } }; @@ -1863,46 +1698,40 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; onContextMenu = (e: React.MouseEvent) => { - if (this.props.isAnnotationOverlay || !ContextMenu.Instance) return; + if (this._props.isAnnotationOverlay || !ContextMenu.Instance) return; const appearance = ContextMenu.Instance.findByDescription('Appearance...'); const appearanceItems = appearance && 'subitems' in appearance ? appearance.subitems : []; - !this.props.Document._isGroup && appearanceItems.push({ description: 'Reset View', event: this.resetView, icon: 'compress-arrows-alt' }); - !this.props.Document._isGroup && appearanceItems.push({ description: 'Toggle Auto Reset View', event: this.toggleResetView, icon: 'compress-arrows-alt' }); + !this.Document.isGroup && appearanceItems.push({ description: 'Reset View', event: this.resetView, icon: 'compress-arrows-alt' }); + !this.Document.isGroup && appearanceItems.push({ description: 'Toggle Auto Reset View', event: this.toggleResetView, icon: 'compress-arrows-alt' }); !Doc.noviceMode && appearanceItems.push({ description: 'Toggle auto arrange', event: () => (this.layoutDoc._autoArrange = !this.layoutDoc._autoArrange), icon: 'compress-arrows-alt', }); - if (this.props.setContentView === emptyFunction) { + if (this._props.setContentViewBox === emptyFunction) { !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); return; } !Doc.noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: 'Reset default note style', event: () => (Doc.UserDoc().defaultTextLayout = undefined), icon: 'eye' }); - appearanceItems.push({ description: `Pin View`, event: () => TabDocView.PinDoc(this.rootDoc, { pinViewport: MarqueeView.CurViewBounds(this.rootDoc, this.props.PanelWidth(), this.props.PanelHeight()) }), icon: 'map-pin' }); + appearanceItems.push({ description: `Pin View`, event: () => this._props.pinToPres(this.Document, { pinViewport: MarqueeView.CurViewBounds(this.dataDoc, this._props.PanelWidth(), this._props.PanelHeight()) }), icon: 'map-pin' }); !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: 'compress-arrows-alt' }); - this.props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' }); + this._props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' }); - this.props.Document._isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: () => this.transcribeStrokes(false), icon: 'font' }); + 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; !appearance && ContextMenu.Instance.addItem({ description: 'Appearance...', subitems: appearanceItems, icon: 'eye' }); - const viewctrls = ContextMenu.Instance.findByDescription('UI Controls...'); - const viewCtrlItems = viewctrls && 'subitems' in viewctrls ? viewctrls.subitems : []; - !Doc.noviceMode ? viewCtrlItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : null; - !viewctrls && ContextMenu.Instance.addItem({ description: 'UI Controls...', subitems: viewCtrlItems, icon: 'eye' }); - const options = ContextMenu.Instance.findByDescription('Options...'); const optionItems = options && 'subitems' in options ? options.subitems : []; - !this.props.isAnnotationOverlay && + !this._props.isAnnotationOverlay && !Doc.noviceMode && optionItems.push({ description: (this._showAnimTimeline ? 'Close' : 'Open') + ' Animation Timeline', event: action(() => (this._showAnimTimeline = !this._showAnimTimeline)), icon: 'eye' }); - 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' }); - this.props.renderDepth && optionItems.push({ description: 'Fit Content Once', event: this.fitContentOnce, icon: 'object-group' }); - // gpt styling - this.props.renderDepth && optionItems.push({ description: 'Style with AI', event: this.gptStyling, icon: 'paint-brush' }); + 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' }); + this._props.renderDepth && optionItems.push({ description: 'Fit Content Once', event: this.fitContentOnce, icon: 'object-group' }); if (!Doc.noviceMode) { optionItems.push({ description: (!Doc.NativeWidth(this.layoutDoc) || !Doc.NativeHeight(this.layoutDoc) ? 'Freeze' : 'Unfreeze') + ' Aspect', event: this.toggleNativeDimensions, icon: 'snowflake' }); } @@ -1913,17 +1742,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @undoBatch - @action - transcribeStrokes = (math: boolean) => { - if (this.props.Document._isGroup && this.props.Document.transcription) { - if (!math) { - const text = StrCast(this.props.Document.transcription); - - const lines = text.split('\n'); - const height = 30 + 15 * lines.length; + transcribeStrokes = () => { + if (this.Document.isGroup && this.Document.transcription) { + const text = StrCast(this.Document.transcription); + const lines = text.split('\n'); + const height = 30 + 15 * lines.length; - this.addDocument(Docs.Create.TextDocument(text, { title: lines[0], x: NumCast(this.layoutDoc.x) + NumCast(this.layoutDoc._width) + 20, y: NumCast(this.layoutDoc.y), _width: 200, _height: height })); - } + this.addDocument(Docs.Create.TextDocument(text, { title: lines[0], x: NumCast(this.layoutDoc.x) + NumCast(this.layoutDoc._width) + 20, y: NumCast(this.layoutDoc.y), _width: 200, _height: height })); } }; @@ -1933,33 +1758,26 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection SnappingManager.clearSnapLines(); }; @action - dragStarting = (snapToDraggedDoc: boolean = false, showGroupDragTarget: boolean, visited = new Set<Doc>()) => { - if (visited.has(this.rootDoc)) return; - visited.add(this.rootDoc); - showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document._isGroup)); - if (this.rootDoc._isGroup && this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView) { - this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.dragStarting(snapToDraggedDoc, false, visited); - } + dragStarting = (snapToDraggedDoc: boolean = false, showGroupDragTarget: boolean = true, visited = new Set<Doc>()) => { + if (visited.has(this.Document)) return; + visited.add(this.Document); + showGroupDragTarget && (this.GroupChildDrag = BoolCast(this.Document.isGroup)); const activeDocs = this.getActiveDocuments(); - const size = this.getTransform().transformDirection(this.props.PanelWidth(), this.props.PanelHeight()); + const size = this.screenToFreeformContentsXf.transformDirection(this._props.PanelWidth(), this._props.PanelHeight()); const selRect = { left: this.panX() - size[0] / 2, top: this.panY() - size[1] / 2, width: size[0], height: size[1] }; const docDims = (doc: Doc) => ({ left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }); const isDocInView = (doc: Doc, rect: { left: number; top: number; width: number; height: number }) => intersectRect(docDims(doc), rect); const snappableDocs = activeDocs.filter(doc => doc.z === undefined && isDocInView(doc, selRect)); // first see if there are any foreground docs to snap to - activeDocs.forEach( - doc => - doc._isGroup && - SnappingManager.GetIsResizing() !== doc && - !DragManager.docsBeingDragged.includes(doc) && - (DocumentManager.Instance.getDocumentView(doc)?.ComponentView as CollectionFreeFormView)?.dragStarting(snapToDraggedDoc, false, visited) - ); + activeDocs + .filter(doc => doc.isGroup && SnappingManager.IsResizing !== doc && !DragManager.docsBeingDragged.includes(doc)) + .forEach(doc => DocumentManager.Instance.getDocumentView(doc)?.ComponentView?.dragStarting?.(snapToDraggedDoc, false, visited)); const horizLines: number[] = []; const vertLines: number[] = []; - const invXf = this.getTransform().inverse(); + const invXf = this.screenToFreeformContentsXf.inverse(); snappableDocs - .filter(doc => !doc._isGroup && (snapToDraggedDoc || (SnappingManager.GetIsResizing() !== doc && !DragManager.docsBeingDragged.includes(doc)))) + .filter(doc => !doc.isGroup && (snapToDraggedDoc || (SnappingManager.IsResizing !== doc && !DragManager.docsBeingDragged.includes(doc)))) .forEach(doc => { const { left, top, width, height } = docDims(doc); const topLeftInScreen = invXf.transformPoint(left, top); @@ -1974,7 +1792,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection incrementalRendering = () => this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])).length !== 0; incrementalRender = action(() => { - if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) { + if (!LightboxView.LightboxDoc || LightboxView.Contains(this.DocumentView?.())) { const layout_unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])); const loadIncrement = 5; for (var i = 0; i < Math.min(layout_unrendered.length, loadIncrement); i++) { @@ -1984,35 +1802,61 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1); }); - get children() { - this.incrementalRender(); - const children = typeof this.props.children === 'function' ? ((this.props.children as any)() as JSX.Element[]) : this.props.children ? [this.props.children] : []; - return [...children, ...this.views, <CollectionFreeFormRemoteCursors {...this.props} key="remoteCursors" />]; - } - @computed get placeholder() { return ( - <div className="collectionfreeformview-placeholder" style={{ background: StrCast(this.Document.backgroundColor) }}> - <span className="collectionfreeformview-placeholderSpan">{this.props.Document.annotationOn ? '' : this.props.Document.title?.toString()}</span> + <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> ); } brushedView = () => this._brushedView; gridColor = () => - DashColor(lightOrDark(this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor))) - .fade(0.6) + DashColor(lightOrDark(this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor))) + .fade(0.5) .toString(); - @computed get marqueeView() { - TraceMobx(); - return this._firstRender ? ( - this.placeholder - ) : ( + @computed 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!!? + PanelWidth={this._props.PanelWidth} + PanelHeight={this._props.PanelHeight} + panX={this.panX} + panY={this.panY} + color={this.gridColor} + nativeDimScaling={this.nativeDim} + zoomScaling={this.zoomScaling} + layoutDoc={this.layoutDoc} + isAnnotationOverlay={this.isAnnotationOverlay} + cachedCenteringShiftX={this.cachedCenteringShiftX} + cachedCenteringShiftY={this.cachedCenteringShiftY} + /> + </div> + ); + } + get pannableContents() { + this.incrementalRender(); + return ( + <CollectionFreeFormPannableContents + Document={this.Document} + brushedView={this.brushedView} + isAnnotationOverlay={this.isAnnotationOverlay} + transform={this.PanZoomCenterXf} + transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.Document._viewTransition, 'string', null))} + viewDefDivClick={this._props.viewDefDivClick}> + {this.props.children ?? null} {/* most likely case of children is document content that's being annoated: eg., an image */} + {this.contentViews} + <CollectionFreeFormRemoteCursors {...this._props} key="remoteCursors" /> + </CollectionFreeFormPannableContents> + ); + } + get marqueeView() { + return ( <MarqueeView - {...this.props} + {...this._props} ref={this._marqueeViewRef} - ungroup={this.rootDoc._isGroup ? this.promoteCollection : undefined} - nudge={this.isAnnotationOverlay || this.props.renderDepth > 0 ? undefined : this.nudge} + ungroup={this.Document.isGroup ? this.promoteCollection : undefined} + nudge={this.isAnnotationOverlay || this._props.renderDepth > 0 ? undefined : this.nudge} addDocTab={this.addDocTab} slowLoadDocuments={this.slowLoadDocuments} trySelectCluster={this.trySelectCluster} @@ -2020,81 +1864,48 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection selectDocuments={this.selectDocuments} addDocument={this.addDocument} addLiveTextDocument={this.addLiveTextBox} - getContainerTransform={this.getContainerTransform} - getTransform={this.getTransform} + getContainerTransform={this.ScreenToLocalBoxXf} + getTransform={this.ScreenToContentsXf} + panXFieldKey={this.panXFieldKey} + panYFieldKey={this.panYFieldKey} isAnnotationOverlay={this.isAnnotationOverlay}> - <div - className="marqueeView-div" - ref={r => { - this._marqueeRef = r; - r?.addEventListener('dashDragAutoScroll', this.onDragAutoScroll as any); - }} - style={{ opacity: this.props.dontRenderDocuments ? 0.7 : undefined }}> - {this.layoutDoc._freeform_backgroundGrid ? ( - <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!!? - PanelWidth={this.props.PanelWidth} - PanelHeight={this.props.PanelHeight} - panX={this.panX} - panY={this.panY} - color={this.gridColor} - nativeDimScaling={this.nativeDim} - zoomScaling={this.zoomScaling} - layoutDoc={this.layoutDoc} - isAnnotationOverlay={this.isAnnotationOverlay} - cachedCenteringShiftX={this.cachedCenteringShiftX} - cachedCenteringShiftY={this.cachedCenteringShiftY} - /> - </div> - ) : null} - <CollectionFreeFormViewPannableContents - rootDoc={this.rootDoc} - brushedView={this.brushedView} - isAnnotationOverlay={this.isAnnotationOverlay} - transform={this.contentTransform} - transition={this._panZoomTransition ? `transform ${this._panZoomTransition}ms` : Cast(this.layoutDoc._viewTransition, 'string', Cast(this.props.DocumentView?.()?.rootDoc._viewTransition, 'string', null))} - viewDefDivClick={this.props.viewDefDivClick}> - {this.children} - </CollectionFreeFormViewPannableContents> - </div> - {this._showAnimTimeline ? <Timeline ref={this._timelineRef} {...this.props} /> : null} + {this.layoutDoc._freeform_backgroundGrid ? this.backgroundGrid : null} + {this.pannableContents} + {this._showAnimTimeline ? <Timeline ref={this._timelineRef} {...this._props} /> : null} </MarqueeView> ); } @computed get nativeDimScaling() { - if (this._firstRender || (this.props.isAnnotationOverlay && !this.props.annotationLayerHostsContent)) return 0; + if (this._firstRender || (this._props.isAnnotationOverlay && !this._props.annotationLayerHostsContent)) return 0; 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.layoutDoc.layout_fitWidth ? wscale : hscale; + 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) => { + 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)), - 2500 - ); + 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.props.ScreenToLocalTransform().translate(-15, -15); + 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.rootDoc[Doc.LayoutFieldKey(this.rootDoc) + '_nativeHeight'], this.nativeHeight); - const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this.props.PanelHeight() / this.nativeDimScaling; - this.props.isSelected() && !scrollable && e.preventDefault(); + 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() { @@ -2117,31 +1928,32 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onDragOver={e => e.preventDefault()} onContextMenu={this.onContextMenu} style={{ - pointerEvents: this.props.isContentActive() && SnappingManager.GetIsDragging() ? 'all' : (this.props.pointerEvents?.() as any), + pointerEvents: this._props.isContentActive() && SnappingManager.IsDragging ? 'all' : (this._props.pointerEvents?.() as any), textAlign: this.isAnnotationOverlay ? 'initial' : undefined, transform: `scale(${this.nativeDimScaling || 1})`, width: `${100 / (this.nativeDimScaling || 1)}%`, - height: this.props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`, + height: this._props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`, }}> {this._lightboxDoc ? ( <div style={{ padding: 15, width: '100%', height: '100%' }}> <DocumentView - {...this.props} + {...this._props} Document={this._lightboxDoc} - DataDoc={undefined} + containerViewPath={this.DocumentView?.().docViewPath} + TemplateDataDocument={undefined} PanelWidth={this.lightboxPanelWidth} PanelHeight={this.lightboxPanelHeight} NativeWidth={returnZero} NativeHeight={returnZero} - onClick={this.onChildClickHandler} + onClickScript={this.onChildClickHandler} onKey={this.onKeyDown} - onDoubleClick={this.onChildDoubleClickHandler} - onBrowseClick={this.onBrowseClickHandler} + onDoubleClickScript={this.onChildDoubleClickHandler} + onBrowseClickScript={this.onBrowseClickHandler} childFilters={this.childDocFilters} childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} - isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive} - isContentActive={this.props.childContentsActive ?? emptyFunction} + isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this.isContentActive} + isContentActive={this._props.childContentsActive ?? emptyFunction} addDocTab={this.addDocTab} ScreenToLocalTransform={this.lightboxScreenToLocal} fitContentsToBox={undefined} @@ -2150,8 +1962,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection </div> ) : ( <> - {this.marqueeView} - {this.props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />} + {this._firstRender ? this.placeholder : this.marqueeView} + {this._props.noOverlay ? null : <CollectionFreeFormOverlayView elements={this.elementFunc} />} {!this.GroupChildDrag ? null : <div className="collectionFreeForm-groupDropper" />} </> )} @@ -2160,139 +1972,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } -interface CollectionFreeFormOverlayViewProps { - elements: () => ViewDefResult[]; -} - -@observer -class CollectionFreeFormOverlayView extends React.Component<CollectionFreeFormOverlayViewProps> { - render() { - return this.props - .elements() - .filter(ele => ele.bounds?.z) - .map(ele => ele.ele); - } -} - -interface CollectionFreeFormViewPannableContentsProps { - rootDoc: Doc; - viewDefDivClick?: ScriptField; - children?: React.ReactNode | undefined; - transition?: string; - isAnnotationOverlay: boolean | undefined; - transform: () => string; - brushedView: () => { panX: number; panY: number; width: number; height: number } | undefined; -} - -@observer -class CollectionFreeFormViewPannableContents extends React.Component<CollectionFreeFormViewPannableContentsProps> { - @computed get presPaths() { - return CollectionFreeFormView.ShowPresPaths ? PresBox.Instance.pathLines(this.props.rootDoc) : null; - } - // rectangle highlight used when following trail/link to a region of a collection that isn't a document - showViewport = (viewport: { panX: number; panY: number; width: number; height: number } | undefined) => - !viewport ? null : ( - <div - className="collectionFreeFormView-brushView" - style={{ - transform: `translate(${viewport.panX}px, ${viewport.panY}px)`, - width: viewport.width, - height: viewport.height, - border: `orange solid ${viewport.width * 0.005}px`, - }} - /> - ); - - render() { - return ( - <div - className={'collectionfreeformview' + (this.props.viewDefDivClick ? '-viewDef' : '-none')} - onScroll={e => { - const target = e.target as any; - if (getComputedStyle(target)?.overflow === 'visible') { - target.scrollTop = target.scrollLeft = 0; // if collection is visible, scrolling messes things up since there are no scroll bars - } - }} - style={{ - transform: this.props.transform(), - transition: this.props.transition, - width: this.props.isAnnotationOverlay ? undefined : 0, // if not an overlay, then this will be the size of the collection, but panning and zooming will move it outside the visible border of the collection and make it selectable. This problem shows up after zooming/panning on a background collection -- you can drag the collection by clicking on apparently empty space outside the collection - }}> - {this.props.children} - {this.presPaths} - {this.showViewport(this.props.brushedView())} - </div> - ); - } -} - -interface CollectionFreeFormViewBackgroundGridProps { - panX: () => number; - panY: () => number; - PanelWidth: () => number; - PanelHeight: () => number; - color: () => string; - isAnnotationOverlay?: boolean; - nativeDimScaling: () => number; - zoomScaling: () => number; - layoutDoc: Doc; - cachedCenteringShiftX: number; - cachedCenteringShiftY: number; -} @observer -class CollectionFreeFormBackgroundGrid extends React.Component<CollectionFreeFormViewBackgroundGridProps> { - chooseGridSpace = (gridSpace: number): number => { - if (!this.props.zoomScaling()) return gridSpace; - const divisions = this.props.PanelWidth() / this.props.zoomScaling() / gridSpace; - return divisions < 90 ? gridSpace : this.chooseGridSpace(gridSpace * 2); - }; +class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> { render() { - const gridSpace = this.chooseGridSpace(NumCast(this.props.layoutDoc['_backgroundGrid-spacing'], 50)); - const shiftX = (this.props.isAnnotationOverlay ? 0 : (-this.props.panX() % gridSpace) - gridSpace) * this.props.zoomScaling(); - const shiftY = (this.props.isAnnotationOverlay ? 0 : (-this.props.panY() % gridSpace) - gridSpace) * this.props.zoomScaling(); - const renderGridSpace = gridSpace * this.props.zoomScaling(); - const w = this.props.PanelWidth() / this.props.nativeDimScaling() + 2 * renderGridSpace; - const h = this.props.PanelHeight() / this.props.nativeDimScaling() + 2 * renderGridSpace; - const strokeStyle = this.props.color(); - return !this.props.nativeDimScaling() ? null : ( - <canvas - className="collectionFreeFormView-grid" - width={w} - height={h} - style={{ transform: `translate(${shiftX}px, ${shiftY}px)` }} - ref={el => { - const ctx = el?.getContext('2d'); - if (ctx) { - const Cx = this.props.cachedCenteringShiftX % renderGridSpace; - const Cy = this.props.cachedCenteringShiftY % renderGridSpace; - ctx.lineWidth = Math.min(1, Math.max(0.5, this.props.zoomScaling())); - ctx.setLineDash(gridSpace > 50 ? [3, 3] : [1, 5]); - ctx.clearRect(0, 0, w, h); - if (ctx) { - ctx.strokeStyle = strokeStyle; - ctx.fillStyle = strokeStyle; - ctx.beginPath(); - if (this.props.zoomScaling() > 1) { - 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); - } - } else { - for (let x = Cx - renderGridSpace; x <= w - Cx; x += renderGridSpace) - for (let y = Cy - renderGridSpace; y <= h - Cy; y += renderGridSpace) { - ctx.fillRect(Math.round(x), Math.round(y), 1, 1); - } - } - ctx.stroke(); - } - } - }} - /> - ); + return this.props.elements().filter(ele => ele.bounds?.z).map(ele => ele.ele); // prettier-ignore } } @@ -2300,48 +1983,85 @@ export function CollectionBrowseClick(dv: DocumentView, clientX: number, clientY const browseTransitionTime = 500; SelectionManager.DeselectAll(); dv && - DocumentManager.Instance.showDocument(dv.rootDoc, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => { + DocumentManager.Instance.showDocument(dv.Document, { zoomScale: 0.8, willZoomCentered: true }, (focused: boolean) => { if (!focused) { - const selfFfview = !dv.rootDoc._isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; - let containers = dv.props.docViewPath(); - let parFfview = dv.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + const selfFfview = !dv.Document.isGroup && dv.ComponentView instanceof CollectionFreeFormView ? dv.ComponentView : undefined; + let containers = dv.containerViewPath?.() ?? []; + let parFfview = dv.CollectionFreeFormView; for (var cont of containers) { - parFfview = parFfview ?? cont.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + parFfview = parFfview ?? cont.CollectionFreeFormView; } - while (parFfview?.rootDoc._isGroup) parFfview = parFfview.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; - const ffview = selfFfview && selfFfview.rootDoc[selfFfview.scaleFieldKey] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview - ffview?.zoomSmoothlyAboutPt(ffview.getTransform().transformPoint(clientX, clientY), ffview?.isAnnotationOverlay ? 1 : 0.5, browseTransitionTime); - Doc.linkFollowHighlight(dv?.props.Document, false); + while (parFfview?.Document.isGroup) parFfview = parFfview.DocumentView?.().CollectionFreeFormView; + const ffview = selfFfview && selfFfview.layoutDoc[selfFfview.scaleFieldKey] !== 0.5 ? selfFfview : parFfview; // if focus doc is a freeform that is not at it's default 0.5 scale, then zoom out on it. Otherwise, zoom out on the parent ffview + ffview?.zoomSmoothlyAboutPt(ffview.screenToFreeformContentsXf.transformPoint(clientX, clientY), ffview?.isAnnotationOverlay ? 1 : 0.5, browseTransitionTime); + Doc.linkFollowHighlight(dv?.Document, false); } }); } ScriptingGlobals.add(CollectionBrowseClick); ScriptingGlobals.add(function nextKeyFrame(readOnly: boolean) { - !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(); + !readOnly && (SelectionManager.Views[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(); }); ScriptingGlobals.add(function prevKeyFrame(readOnly: boolean) { - !readOnly && (SelectionManager.Views()[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true); + !readOnly && (SelectionManager.Views[0].ComponentView as CollectionFreeFormView)?.changeKeyFrame(true); }); ScriptingGlobals.add(function curKeyFrame(readOnly: boolean) { - const selView = SelectionManager.Views(); + const selView = SelectionManager.Views; if (readOnly) return selView[0].ComponentView?.getKeyFrameEditing?.() ? Colors.MEDIUM_BLUE : 'transparent'; runInAction(() => selView[0].ComponentView?.setKeyFrameEditing?.(!selView[0].ComponentView?.getKeyFrameEditing?.())); }); ScriptingGlobals.add(function pinWithView(pinContent: boolean) { - SelectionManager.Views().forEach(view => - view.props.pinToPres(view.rootDoc, { - currentFrame: Cast(view.rootDoc.currentFrame, 'number', null), + SelectionManager.Views.forEach(view => + view._props.pinToPres(view.Document, { + currentFrame: Cast(view.Document.currentFrame, 'number', null), pinData: { poslayoutview: pinContent, dataview: pinContent, }, - pinViewport: MarqueeView.CurViewBounds(view.rootDoc, view.props.PanelWidth(), view.props.PanelHeight()), + pinViewport: MarqueeView.CurViewBounds(view.Document, view._props.PanelWidth(), view._props.PanelHeight()), }) ); }); ScriptingGlobals.add(function bringToFront() { - SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc)); + SelectionManager.Views.forEach(view => view.CollectionFreeFormView?.bringToFront(view.Document)); }); ScriptingGlobals.add(function sendToBack(doc: Doc) { - SelectionManager.Views().forEach(view => view.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView.bringToFront(view.rootDoc, true)); + SelectionManager.Views.forEach(view => view.CollectionFreeFormView?.bringToFront(view.Document, true)); +}); +ScriptingGlobals.add(function datavizFromSchema(doc: Doc) { + SelectionManager.Views.forEach(view => { + if (!view.layoutDoc.schema_columnKeys) { + view.layoutDoc.schema_columnKeys = new List<string>(['title', 'type', 'author', 'author_date']); + } + const keys = Cast(view.layoutDoc.schema_columnKeys, listSpec('string'))?.filter(key => key != 'text'); + if (!keys) return; + + const children = DocListCast(view.Document[Doc.LayoutFieldKey(view.Document)]); + let csvRows = []; + csvRows.push(keys.join(',')); + for (let i = 0; i < children.length; i++) { + let eachRow = []; + for (let j = 0; j < keys.length; j++) { + var cell = children[i][keys[j]]; + if (cell && (cell as string)) cell = cell.toString().replace(/\,/g, ''); + eachRow.push(cell); + } + csvRows.push(eachRow); + } + const blob = new Blob([csvRows.join('\n')], { type: 'text/csv' }); + const options = { x: 0, y: -300, title: 'schemaTable', _width: 300, _height: 100, type: 'text/csv' }; + const file = new File([blob], 'schemaTable', options); + const loading = Docs.Create.LoadingDocument(file, options); + loading.presentation_openInLightbox = true; + DocUtils.uploadFileToDoc(file, {}, loading); + + if (view.ComponentView?.addDocument) { + // loading.dataViz_fromSchema = true; + loading.dataViz_asSchema = view.layoutDoc; + SchemaCSVPopUp.Instance.setView(view); + SchemaCSVPopUp.Instance.setTarget(view.layoutDoc); + SchemaCSVPopUp.Instance.setDataVizDoc(loading); + SchemaCSVPopUp.Instance.setVisible(true); + } + }); }); |
