import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; import { ComputedField } from '../../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; import { numberRange, OmitKeys } from '../../../Utils'; import { DocumentManager } from '../../util/DocumentManager'; import { SelectionManager } from '../../util/SelectionManager'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { DocComponent } from '../DocComponent'; import { StyleProp } from '../StyleProvider'; import './CollectionFreeFormDocumentView.scss'; import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; import React = require('react'); import { ScriptingGlobals } from '../../util/ScriptingGlobals'; export interface CollectionFreeFormDocumentViewWrapperProps extends DocumentViewProps { x: number; y: number; z: number; width: number; height: number; zIndex?: number; autoDim?: number; // 1 means use Panel Width/Height, 0 means use width/height rotation?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; transition?: string; dataTransition?: string; RenderCutoffProvider: (doc: Doc) => boolean; CollectionFreeFormView: CollectionFreeFormView; } @observer export class CollectionFreeFormDocumentViewWrapper extends DocComponent() implements CollectionFreeFormDocumentViewProps { @observable X = this.props.x; @observable Y = this.props.y; @observable Z = this.props.z; @observable ZIndex = this.props.zIndex; @observable Rotation = this.props.rotation; @observable Opacity = this.props.opacity; @observable BackgroundColor = this.props.backgroundColor; @observable Color = this.props.color; @observable Highlight = this.props.highlight; @observable Width = this.props.width; @observable Height = this.props.height; @observable AutoDim = this.props.autoDim; @observable Transition = this.props.transition; @observable DataTransition = this.props.dataTransition; CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking @computed get WrapperKeys() { return Object.keys(this).filter(key => key.startsWith('w_')).map(key => key.replace('w_', '')) .map(key => ({upper:key, lower:key[0].toLowerCase() + key.substring(1)})); // prettier-ignore } // wrapper functions around prop fields that have been converted to observables to keep 'props' from ever changing. // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes w_X = () => this.X; // prettier-ignore w_Y = () => this.Y; // prettier-ignore w_Z = () => this.Z; // prettier-ignore w_ZIndex = () => this.ZIndex ?? NumCast(this.props.Document.zIndex); // prettier-ignore w_Rotation = () => this.Rotation ?? NumCast(this.props.Document._rotation); // prettier-ignore w_Opacity = () => this.Opacity; // prettier-ignore w_BackgroundColor = () => this.BackgroundColor ?? Cast(this.props.Document._backgroundColor, 'string', null); // prettier-ignore w_Color = () => this.Color ?? Cast(this.props.Document._color, 'string', null); // prettier-ignore w_Highlight = () => this.Highlight; // prettier-ignore w_Width = () => this.Width; // prettier-ignore w_Height = () => this.Height; // prettier-ignore w_AutoDim = () => this.AutoDim; w_Transition = () => this.Transition; // prettier-ignore w_DataTransition = () => this.DataTransition; // prettier-ignore PanelWidth = () => this.props.autoDim ? this.props.PanelWidth?.() : this.Width; // prettier-ignore PanelHeight = () => this.props.autoDim ? this.props.PanelHeight?.() : this.Height; // prettier-ignore @action componentDidUpdate() { this.WrapperKeys.forEach(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower])); } render() { const layoutProps = this.WrapperKeys.reduce((val, keys) => [(val['w_' + keys.upper] = (this as any)['w_' + keys.upper]), val][1], {} as { [key: string]: Function }); return ( keys.lower) ).omit} // prettier-ignore {...layoutProps} PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} /> ); } } export interface CollectionFreeFormDocumentViewProps { w_X: () => number; w_Y: () => number; w_Z: () => number; w_ZIndex?: () => number; w_Rotation?: () => number; w_Color: () => string; w_BackgroundColor: () => string; w_Opacity: () => number | undefined; w_Highlight: () => boolean | undefined; w_Transition: () => string | undefined; w_Width: () => number; w_Height: () => number; w_DataTransition: () => string | undefined; PanelWidth: () => number; PanelHeight: () => number; RenderCutoffProvider: (doc: Doc) => boolean; CollectionFreeFormView: CollectionFreeFormView; } @observer export class CollectionFreeFormDocumentView extends DocComponent() { get displayName() { // this makes mobx trace() statements more descriptive return 'CollectionFreeFormDocumentView(' + this.Document.title + ')'; } // prettier-ignore public static animFields: { key: string; val?: number }[] = [ { key: 'x' }, { key: 'y' }, { key: 'opacity', val: 1 }, { key: '_height' }, { key: '_width' }, { key: '_rotation', val: 0 }, { key: '_layout_scrollTop' }, { key: '_currentFrame' }, { key: 'freeform_scale', val: 1 }, { key: 'freeform_panX' }, { key: 'freeform_panY' }, ]; // fields that are configured to be animatable using animation frames public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames get CollectionFreeFormView() { return this.props.CollectionFreeFormView; } styleProvider = (doc: Doc | undefined, props: Opt, property: string) => { if (doc === this.layoutDoc) { switch (property) { case StyleProp.Opacity: return this.props.w_Opacity(); // only change the opacity for this specific document, not its children case StyleProp.BackgroundColor: return this.props.w_BackgroundColor(); case StyleProp.Color: return this.props.w_Color(); } // prettier-ignore } return this.props.styleProvider?.(doc, props, property); }; public static getValues(doc: Doc, time: number, fillIn: boolean = true) { return CollectionFreeFormDocumentView.animFields.reduce((p, val) => { p[val.key] = Cast(doc[`${val.key}_indexed`], listSpec('number'), fillIn ? [NumCast(doc[val.key], val.val)] : []).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as number); return p; }, {} as { [val: string]: Opt }); } public static getStringValues(doc: Doc, time: number) { return CollectionFreeFormDocumentView.animStringFields.reduce((p, val) => { p[val] = Cast(doc[`${val}_indexed`], listSpec('string'), [StrCast(doc[val])]).reduce((p, v, i) => ((i <= Math.round(time) && v !== undefined) || p === undefined ? v : p), undefined as any as string); return p; }, {} as { [val: string]: Opt }); } public static setStringValues(time: number, d: Doc, vals: { [val: string]: Opt }) { const timecode = Math.round(time); Object.keys(vals).forEach(val => { const findexed = Cast(d[`${val}_indexed`], listSpec('string'), []).slice(); findexed[timecode] = vals[val] as any as string; d[`${val}_indexed`] = new List(findexed); }); } public static setValues(time: number, d: Doc, vals: { [val: string]: Opt }) { const timecode = Math.round(time); Object.keys(vals).forEach(val => { const findexed = Cast(d[`${val}_indexed`], listSpec('number'), []).slice(); findexed[timecode] = vals[val] as any as number; d[`${val}_indexed`] = new List(findexed); }); } public static gotoKeyFrame(doc: Doc, newFrame: number) { if (doc) { const childDocs = DocListCast(doc[Doc.LayoutFieldKey(doc)]); const currentFrame = Cast(doc._currentFrame, 'number', null); if (currentFrame === undefined) { doc._currentFrame = 0; CollectionFreeFormDocumentView.setupKeyframes(childDocs, 0); } CollectionFreeFormView.updateKeyframe(undefined, [...childDocs, doc], currentFrame || 0); doc._currentFrame = newFrame === undefined ? 0 : Math.max(0, newFrame); } } public static setupKeyframes(docs: Doc[], currTimecode: number, makeAppear: boolean = false) { docs.forEach(doc => { if (doc.appearFrame === undefined) doc.appearFrame = currTimecode; if (!doc['opacity_indexed']) { // opacity is unlike other fields because it's value should not be undefined before it appears to enable it to fade-in doc['opacity_indexed'] = new List(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1))); } CollectionFreeFormDocumentView.animFields.forEach(val => (doc[val.key] = ComputedField.MakeInterpolatedNumber(val.key, 'activeFrame', doc, currTimecode, val.val))); CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode))); CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (doc[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', doc, currTimecode))); const targetDoc = doc; // data fields, like rtf 'text' exist on the data doc, so //doc !== targetDoc && (targetDoc.embedContainer = doc.embedContainer); // the computed fields don't see the layout doc -- need to copy the embedContainer to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!) targetDoc.activeFrame = ComputedField.MakeFunction('this.embedContainer?._currentFrame||0'); targetDoc.dataTransition = 'inherit'; }); } @action public float = () => { const topDoc = this.Document; const containerDocView = this.props.docViewPath().lastElement(); const screenXf = containerDocView?.screenToLocalTransform(); if (screenXf) { SelectionManager.DeselectAll(); if (topDoc.z) { const spt = screenXf.inverse().transformPoint(NumCast(topDoc.x), NumCast(topDoc.y)); topDoc.z = 0; topDoc.x = spt[0]; topDoc.y = spt[1]; this.props.removeDocument?.(topDoc); this.props.addDocTab(topDoc, OpenWhere.inParentFromScreen); } else { const spt = this.screenToLocalTransform().inverse().transformPoint(0, 0); const fpt = screenXf.transformPoint(spt[0], spt[1]); topDoc.z = 1; topDoc.x = fpt[0]; topDoc.y = fpt[1]; } setTimeout(() => SelectionManager.SelectView(DocumentManager.Instance.getDocumentView(topDoc, containerDocView), false), 0); } }; nudge = (x: number, y: number) => { const [locX, locY] = this.props.ScreenToLocalTransform().transformDirection(x, y); this.props.Document.x = this.props.w_X() + locX; this.props.Document.y = this.props.w_Y() + locY; }; screenToLocalTransform = () => this.props .ScreenToLocalTransform() .translate(-this.props.w_X(), -this.props.w_Y()) .rotateDeg(-(this.props.w_Rotation?.() || 0)); returnThis = () => this; /// this indicates whether the doc view is activated because of its relationshop to a group // 'group' - this is a group that is activated because it's on an active canvas, but is not part of some other group // 'child' - this is a group child that is activated because its containing group is activated // 'inactive' - this is a group child but it is not active // undefined - this is not activated by a group isGroupActive = () => { if (this.CollectionFreeFormView.isAnyChildContentActive()) return undefined; const isGroup = this.dataDoc.isGroup && (!this.layoutDoc.backgroundColor || this.layoutDoc.backgroundColor === 'transparent'); return isGroup ? (this.props.isDocumentActive?.() ? 'group' : this.props.isGroupActive?.() ? 'child' : 'inactive') : this.props.isGroupActive?.() ? 'child' : undefined; }; public static CollectionFreeFormDocViewClassName = 'collectionFreeFormDocumentView-container'; render() { TraceMobx(); const passOnProps = OmitKeys(this.props, Object.keys(this.props).filter(key => key.startsWith('w_'))).omit; // prettier-ignore return (
{this.props.RenderCutoffProvider(this.props.Document) ? (
) : ( )}
); } } ScriptingGlobals.add(function gotoFrame(doc: any, newFrame: any) { CollectionFreeFormDocumentView.gotoKeyFrame(doc, newFrame); });