aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/nodes/CollectionFreeFormDocumentView.tsx')
-rw-r--r--src/client/views/nodes/CollectionFreeFormDocumentView.tsx313
1 files changed, 190 insertions, 123 deletions
diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
index f9afe4d53..0ae4ed62c 100644
--- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
+++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx
@@ -1,42 +1,146 @@
-import { action, computed, observable } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
-import { Doc, Opt } from '../../../fields/Doc';
+import * as React from 'react';
+import { OmitKeys, numberRange } from '../../../Utils';
+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 { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SelectionManager } from '../../util/SelectionManager';
-import { Transform } from '../../util/Transform';
-import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { DocComponent } from '../DocComponent';
+import { ObservableReactComponent } from '../ObservableReactComponent';
import { StyleProp } from '../StyleProvider';
+import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import './CollectionFreeFormDocumentView.scss';
import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView';
-import React = require('react');
+import { FieldViewProps } from './FieldView';
-export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
- dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; rotation?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined;
- sizeProvider?: (doc: Doc, replica: string) => { width: number; height: number } | undefined;
- renderCutoffProvider: (doc: Doc) => boolean;
+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;
- replica: string;
+ RenderCutoffProvider: (doc: Doc) => boolean;
+ CollectionFreeFormView: CollectionFreeFormView;
+}
+@observer
+export class CollectionFreeFormDocumentViewWrapper extends ObservableReactComponent<CollectionFreeFormDocumentViewWrapperProps> {
+ constructor(props: CollectionFreeFormDocumentViewWrapperProps) {
+ super(props);
+ makeObservable(this);
+ }
+ @observable X = this.props.x;
+ @observable Y = this.props.y;
+ @observable Z = this.props.z;
+ @observable ZIndex = this.props.zIndex;
+ @observable Rotation = this.props.rotation;
+ @observable Opacity = this.props.opacity;
+ @observable BackgroundColor = this.props.backgroundColor;
+ @observable Color = this.props.color;
+ @observable Highlight = this.props.highlight;
+ @observable Width = this.props.width;
+ @observable Height = this.props.height;
+ @observable AutoDim = this.props.autoDim;
+ @observable Transition = this.props.transition;
+ @observable DataTransition = this.props.dataTransition;
+ CollectionFreeFormView = this.props.CollectionFreeFormView; // needed for type checking
+ RenderCutoffProvider = this.props.RenderCutoffProvider; // needed for type checking
+
+ get Document() {
+ return this._props.Document;
+ }
+ @computed get WrapperKeys() {
+ return Object.keys(this).filter(key => key.startsWith('w_')).map(key => key.replace('w_', ''))
+ .map(key => ({upper:key, lower:key[0].toLowerCase() + key.substring(1)})); // prettier-ignore
+ }
+
+ // wrapper functions around prop fields that have been converted to observables to keep 'props' from ever changing.
+ // this way, downstream code only invalidates when it uses a specific prop, not when any prop changes
+ w_X = () => this.X; // prettier-ignore
+ w_Y = () => this.Y; // prettier-ignore
+ w_Z = () => this.Z; // prettier-ignore
+ w_ZIndex = () => this.ZIndex ?? NumCast(this.Document.zIndex); // prettier-ignore
+ w_Rotation = () => this.Rotation ?? NumCast(this.Document._rotation); // prettier-ignore
+ w_Opacity = () => this.Opacity; // prettier-ignore
+ w_BackgroundColor = () => this.BackgroundColor ?? Cast(this.Document._backgroundColor, 'string', null); // prettier-ignore
+ w_Color = () => this.Color ?? Cast(this.Document._color, 'string', null); // prettier-ignore
+ w_Highlight = () => this.Highlight; // prettier-ignore
+ w_Width = () => this.Width; // prettier-ignore
+ w_Height = () => this.Height; // prettier-ignore
+ w_AutoDim = () => this.AutoDim; // prettier-ignore
+ w_Transition = () => this.Transition; // prettier-ignore
+ 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
+
+ componentDidUpdate(prevProps: Readonly<React.PropsWithChildren<CollectionFreeFormDocumentViewWrapperProps & { fieldKey: string }>>) {
+ super.componentDidUpdate(prevProps);
+ this.WrapperKeys.forEach(action(keys => ((this as any)[keys.upper] = (this.props as any)[keys.lower])));
+ }
+ render() {
+ const layoutProps = this.WrapperKeys.reduce((val, keys) => [(val['w_' + keys.upper] = (this as any)['w_' + keys.upper]), val][1], {} as { [key: string]: Function });
+ return (
+ <CollectionFreeFormDocumentView
+ {...OmitKeys(this._props, this.WrapperKeys.map(keys => keys.lower) ).omit} // prettier-ignore
+ {...layoutProps}
+ PanelWidth={this.PanelWidth}
+ PanelHeight={this.PanelHeight}
+ />
+ );
+ }
+}
+export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps {
+ w_X: () => number;
+ w_Y: () => number;
+ w_Z: () => number;
+ w_ZIndex?: () => number;
+ w_Rotation?: () => number;
+ w_Color: () => string;
+ w_BackgroundColor: () => string;
+ w_Opacity: () => number | undefined;
+ w_Highlight: () => boolean | undefined;
+ w_Transition: () => string | undefined;
+ w_Width: () => number;
+ w_Height: () => number;
+ w_DataTransition: () => string | undefined;
+ PanelWidth: () => number;
+ PanelHeight: () => number;
+ RenderCutoffProvider: (doc: Doc) => boolean;
CollectionFreeFormView: CollectionFreeFormView;
}
@observer
export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeFormDocumentViewProps>() {
+ constructor(props: CollectionFreeFormDocumentViewProps) {
+ super(props);
+ makeObservable(this);
+ }
+ get displayName() { // this makes mobx trace() statements more descriptive
+ return 'CollectionFreeFormDocumentView(' + this.Document.title + ')';
+ } // prettier-ignore
public static animFields: { key: string; val?: number }[] = [
- { key: '_height' },
- { key: '_width' },
{ key: 'x' },
{ key: 'y' },
+ { key: 'opacity', val: 1 },
+ { key: '_height' },
+ { key: '_width' },
{ key: '_rotation', val: 0 },
{ key: '_layout_scrollTop' },
- { key: 'opacity', val: 1 },
{ key: '_currentFrame' },
{ key: 'freeform_scale', val: 1 },
{ key: 'freeform_panX' },
@@ -44,124 +148,93 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
]; // 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
- @observable _animPos: number[] | undefined = undefined;
- @observable _contentView: DocumentView | undefined | null;
- get displayName() {
- // this makes mobx trace() statements more descriptive
- return 'CollectionFreeFormDocumentView(' + this.rootDoc.title + ')';
- }
- get transform() {
- return `translate(${this.X}px, ${this.Y}px) rotate(${NumCast(this.Rot, this.Rot)}deg)`;
- }
- get X() {
- return this.dataProvider?.x ?? NumCast(this.Document.x);
- }
- get Y() {
- return this.dataProvider?.y ?? NumCast(this.Document.y);
- }
- get ZInd() {
- return this.dataProvider?.zIndex ?? NumCast(this.Document.zIndex);
- }
- get Rot() {
- return this.dataProvider?.rotation ?? NumCast(this.Document._rotation);
- }
- get Opacity() {
- return this.dataProvider?.opacity;
- }
- get BackgroundColor() {
- return this.dataProvider?.backgroundColor ?? Cast(this.Document._backgroundColor, 'string', null);
- }
- get Color() {
- return this.dataProvider?.color ?? Cast(this.Document._color, 'string', null);
- }
- @computed get dataProvider() {
- return this.props.dataProvider?.(this.props.Document, this.props.replica);
- }
- @computed get sizeProvider() {
- return this.props.sizeProvider?.(this.props.Document, this.props.replica);
+ get CollectionFreeFormView() {
+ return this._props.CollectionFreeFormView;
}
- styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => {
+ styleProvider = (doc: Doc | undefined, props: Opt<FieldViewProps>, property: string) => {
if (doc === this.layoutDoc) {
- // prettier-ignore
switch (property) {
- case StyleProp.Opacity: return this.Opacity; // only change the opacity for this specific document, not its children
- case StyleProp.BackgroundColor: return this.BackgroundColor;
- case StyleProp.Color: return this.Color;
- }
+ 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);
+ 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<number> });
+ 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<number> }
+ );
}
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<string> });
+ 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<string> }
+ );
}
public static setStringValues(time: number, d: Doc, vals: { [val: string]: Opt<string> }) {
const timecode = Math.round(time);
Object.keys(vals).forEach(val => {
- const findexed = Cast(d[`${val}-indexed`], listSpec('string'), []).slice();
+ const findexed = Cast(d[`${val}_indexed`], listSpec('string'), []).slice();
findexed[timecode] = vals[val] as any as string;
- d[`${val}-indexed`] = new List<string>(findexed);
+ d[`${val}_indexed`] = new List<string>(findexed);
});
}
public static setValues(time: number, d: Doc, vals: { [val: string]: Opt<number> }) {
const timecode = Math.round(time);
Object.keys(vals).forEach(val => {
- const findexed = Cast(d[`${val}-indexed`], listSpec('number'), []).slice();
+ const findexed = Cast(d[`${val}_indexed`], listSpec('number'), []).slice();
findexed[timecode] = vals[val] as any as number;
- d[`${val}-indexed`] = new List<number>(findexed);
+ d[`${val}_indexed`] = new List<number>(findexed);
});
}
-
- public static setupZoom(doc: Doc, targDoc: Doc) {
- const width = new List<number>();
- const height = new List<number>();
- const top = new List<number>();
- const left = new List<number>();
- width.push(NumCast(targDoc._width));
- height.push(NumCast(targDoc._height));
- top.push(NumCast(targDoc._height) / -2);
- left.push(NumCast(targDoc._width) / -2);
- doc['viewfinder-width-indexed'] = width;
- doc['viewfinder-height-indexed'] = height;
- doc['viewfinder-top-indexed'] = top;
- doc['viewfinder-left-indexed'] = left;
+ 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']) {
+ 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<number>(numberRange(currTimecode + 1).map(t => (!doc.z && makeAppear && t < NumCast(doc.appearFrame) ? 0 : 1)));
+ doc['opacity_indexed'] = new List<number>(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('self.embedContainer?._currentFrame||0');
+ targetDoc.activeFrame = ComputedField.MakeFunction('this.embedContainer?._currentFrame||0');
targetDoc.dataTransition = 'inherit';
});
}
- @action public float = () => {
- const topDoc = this.rootDoc;
- const containerDocView = this.props.docViewPath().lastElement();
- const screenXf = containerDocView?.screenToLocalTransform();
+ float = () => {
+ const topDoc = this.Document;
+ const containerDocView = this._props.containerViewPath?.().lastElement();
+ const screenXf = containerDocView?.screenToContentsTransform();
if (screenXf) {
SelectionManager.DeselectAll();
if (topDoc.z) {
@@ -169,8 +242,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
topDoc.z = 0;
topDoc.x = spt[0];
topDoc.y = spt[1];
- this.props.removeDocument?.(topDoc);
- this.props.addDocTab(topDoc, OpenWhere.inParentFromScreen);
+ 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]);
@@ -182,16 +255,16 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
}
};
- dragEnding = () => this.props.CollectionFreeFormView?.dragEnding();
- dragStarting = () => this.props.CollectionFreeFormView?.dragStarting(false, true);
-
nudge = (x: number, y: number) => {
- this.props.Document.x = NumCast(this.props.Document.x) + x;
- this.props.Document.y = NumCast(this.props.Document.y) + y;
+ const [locX, locY] = this._props.ScreenToLocalTransform().transformDirection(x, y);
+ this.Document.x = this._props.w_X() + locX;
+ this.Document.y = this._props.w_Y() + locY;
};
- panelWidth = () => this.sizeProvider?.width || this.props.PanelWidth?.();
- panelHeight = () => this.sizeProvider?.height || this.props.PanelHeight?.();
- screenToLocalTransform = (): Transform => this.props.ScreenToLocalTransform().translate(-this.X, -this.Y);
+ 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
@@ -200,40 +273,34 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF
// 'inactive' - this is a group child but it is not active
// undefined - this is not activated by a group
isGroupActive = () => {
- if (this.props.CollectionFreeFormView.isAnyChildContentActive()) return undefined;
- const isGroup = this.rootDoc._isGroup && (!this.rootDoc.backgroundColor || this.rootDoc.backgroundColor === 'transparent');
- return isGroup ? (this.props.isDocumentActive?.() ? 'group' : this.props.isGroupActive?.() ? 'child' : 'inactive') : this.props.isGroupActive?.() ? 'child' : undefined;
+ 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 divProps: DocumentViewProps = {
- ...this.props,
- CollectionFreeFormDocumentView: this.returnThis,
- styleProvider: this.styleProvider,
- ScreenToLocalTransform: this.screenToLocalTransform,
- PanelWidth: this.panelWidth,
- PanelHeight: this.panelHeight,
- isGroupActive: this.isGroupActive,
- };
+ const passOnProps = OmitKeys(this._props, Object.keys(this._props).filter(key => key.startsWith('w_'))).omit; // prettier-ignore
return (
<div
- className="collectionFreeFormDocumentView-container"
+ className={CollectionFreeFormDocumentView.CollectionFreeFormDocViewClassName}
style={{
- width: this.panelWidth(),
- height: this.panelHeight(),
- transform: this.transform,
- transformOrigin: '50% 50%',
- transition: this.dataProvider?.transition ?? (this.props.dataTransition ? this.props.dataTransition : this.dataProvider ? this.dataProvider.transition : StrCast(this.layoutDoc.dataTransition)),
- zIndex: this.ZInd,
- display: this.sizeProvider?.width ? undefined : 'none',
- pointerEvents: 'none',
+ width: this._props.PanelWidth(),
+ height: this._props.PanelHeight(),
+ transform: `translate(${this._props.w_X()}px, ${this._props.w_Y()}px) rotate(${NumCast(this._props.w_Rotation?.())}deg)`,
+ transition: this._props.w_Transition?.() ?? (this._props.w_DataTransition?.() || this._props.w_Transition?.()),
+ zIndex: this._props.w_ZIndex?.(),
+ display: this._props.w_Width?.() ? undefined : 'none',
}}>
- {this.props.renderCutoffProvider(this.props.Document) ? (
- <div style={{ position: 'absolute', width: this.panelWidth(), height: this.panelHeight(), background: 'lightGreen' }} />
+ {this._props.RenderCutoffProvider(this.Document) ? (
+ <div style={{ position: 'absolute', width: this._props.PanelWidth(), height: this._props.PanelHeight(), background: 'lightGreen' }} />
) : (
- <DocumentView {...divProps} ref={action((r: DocumentView | null) => (this._contentView = r))} />
+ <DocumentView {...passOnProps} CollectionFreeFormDocumentView={this.returnThis} styleProvider={this.styleProvider} ScreenToLocalTransform={this.screenToLocalTransform} isGroupActive={this.isGroupActive} />
)}
</div>
);
}
}
+ScriptingGlobals.add(function gotoFrame(doc: any, newFrame: any) {
+ CollectionFreeFormDocumentView.gotoKeyFrame(doc, newFrame);
+});