From 3381bbb0ef5160707513f4bbbe551ca551b64b0d Mon Sep 17 00:00:00 2001 From: bobzel Date: Sat, 13 Nov 2021 10:38:53 -0500 Subject: change isContentActive to a tri-state to allow turning on/off and default - fixes issues with videobox and others so that content can be turned off reliably. added annotation overlay for treeViews for ppt like slides. lots of fixes to tree view to get layout to be more robust. --- src/client/views/collections/TreeView.tsx | 49 +++++++++++++++++++------------ 1 file changed, 30 insertions(+), 19 deletions(-) (limited to 'src/client/views/collections/TreeView.tsx') diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 7f2128230..a266c301f 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -81,7 +81,7 @@ export class TreeView extends React.Component { static _openLevelScript: Opt; private _header: React.RefObject = React.createRef(); private _tref = React.createRef(); - private _docRef: Opt; + @observable _docRef: Opt; private _selDisposer: Opt; private _editTitleScript: (() => ScriptField) | undefined; private _openScript: (() => ScriptField) | undefined; @@ -116,7 +116,8 @@ export class TreeView extends React.Component { @computed get childLinks() { return this.childDocList("links"); } @computed get childAliases() { return this.childDocList("aliases"); } @computed get childAnnos() { return this.childDocList(this.fieldKey + "-annotations"); } - @computed get selected() { return SelectionManager.Views().lastElement()?.props.Document === this.props.document; } + @computed get selected() { return SelectionManager.IsSelected(this._docRef); } + // SelectionManager.Views().lastElement()?.props.Document === this.props.document; } childDocList(field: string) { const layout = Cast(Doc.LayoutField(this.doc), Doc, null); @@ -125,7 +126,12 @@ export class TreeView extends React.Component { DocListCastOrNull(this.doc[field]); // otherwise use the document's data field } @undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => { - return this.doc !== target && this.props.removeDoc?.(doc) === true && addDoc(doc); + if (this.doc !== target && addDoc !== returnFalse) { // bcz: this should all be running in a Temp undo batch instead of hackily testing for returnFalse + if (this.props.removeDoc?.(doc) === true) { + return addDoc(doc); + } + } + return false; } @undoBatch @action remove = (doc: Doc | Doc[], key: string) => { this.props.treeView.props.select(false); @@ -141,8 +147,10 @@ export class TreeView extends React.Component { this._editTitle = false; } else if (docView.isSelected()) { + const doc = docView.Document; + SelectionManager.SelectSchemaViewDoc(doc); this._editTitle = true; - this._selDisposer = reaction(() => docView.isSelected(), sel => !sel && this.setEditTitle(undefined)); + this._selDisposer = reaction(() => SelectionManager.SelectedSchemaDoc(), seldoc => seldoc !== doc && this.setEditTitle(undefined)); } else { docView.select(false); } @@ -222,7 +230,7 @@ export class TreeView extends React.Component { public static makeTextBullet() { const bullet = Docs.Create.TextDocument("-text-", { layout: CollectionView.LayoutString("data"), - title: "-title-", "sidebarColor": "transparent", "sidebarViewType": CollectionViewType.Freeform, + title: "-title-", treeViewExpandedViewLock: true, treeViewExpandedView: "data", _viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewType: "outline", x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, backgroundColor: "transparent", _width: 1000, _height: 10 @@ -266,23 +274,25 @@ export class TreeView extends React.Component { e.stopPropagation(); } const docDragData = de.complete.docDragData; - if (docDragData) { - e.stopPropagation(); + if (docDragData && pt[0] < rect.left + rect.width) { if (docDragData.draggedDocuments[0] === this.doc) return true; - this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document); + if (this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document)) { + e.stopPropagation(); + } } } dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) { const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before); - const canAdd = !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add") || forceAdd; + const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add")) || forceAdd; const localAdd = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) && ((doc.context = this.doc.context) || true) ? true : false; const addDoc = !inside ? parentAddDoc : (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc), true as boolean); const move = (!dropAction || dropAction === "proto" || dropAction === "move" || dropAction === "same") && moveDocument; if (canAdd) { - UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false)); + return UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false)); } + return false; } refTransform = (ref: HTMLDivElement | undefined | null) => { @@ -432,7 +442,7 @@ export class TreeView extends React.Component { ; } - return
    {this.renderEmbeddedDocument(false)}
; // "layout" + return
    { e.preventDefault(); e.stopPropagation(); }}>{this.renderEmbeddedDocument(false, returnFalse)}
; // "layout" } get onCheckedClick() { return this.doc.type === DocumentType.COL ? undefined : this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); } @@ -581,6 +591,7 @@ export class TreeView extends React.Component { } titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth() - 2 * treeBulletWidth())); + return18 = () => 18; /** * Renders the EditableView title element for placement into the tree. */ @@ -636,10 +647,10 @@ export class TreeView extends React.Component { moveDocument={this.move} removeDocument={this.props.removeDoc} ScreenToLocalTransform={this.getTransform} - NativeHeight={() => 18} + NativeHeight={this.return18} NativeWidth={this.titleWidth} PanelWidth={this.titleWidth} - PanelHeight={() => 18} + PanelHeight={this.return18} contextMenuItems={this.contextMenuItems} renderDepth={1} isContentActive={this.props.isContentActive} @@ -679,6 +690,7 @@ export class TreeView extends React.Component { renderBulletHeader = (contents: JSX.Element, editing: boolean) => { return <>
{ } - renderEmbeddedDocument = (asText: boolean) => { + renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => { const layout = StrCast(Doc.LayoutField(this.layoutDoc)); const isExpandable = layout.includes(FormattedTextBox.name) || layout.includes(SliderBox.name); const panelWidth = asText || isExpandable ? this.rtfWidth : this.expandPanelWidth; @@ -704,8 +716,8 @@ export class TreeView extends React.Component { NativeWidth={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfWidth : undefined} NativeHeight={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfHeight : undefined} LayoutTemplateString={asText ? FormattedTextBox.LayoutString("text") : undefined} - isContentActive={asText ? this.props.isContentActive : returnFalse} - isDocumentActive={asText ? this.props.isContentActive : returnFalse} + isContentActive={isActive} + isDocumentActive={isActive} styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider} hideTitle={asText} fitContentsToDoc={returnTrue} @@ -749,7 +761,7 @@ export class TreeView extends React.Component { @computed get renderDocumentAsHeader() { return <> {this.renderBullet} - {this.renderEmbeddedDocument(true)} + {this.renderEmbeddedDocument(true, this.props.isContentActive)} ; } @@ -776,13 +788,12 @@ export class TreeView extends React.Component { return this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? "<" + this.doc.title + ">" : // just print the title of documents we've previously rendered in this hierarchical path to avoid cycles
this.props.isContentActive(true) && SelectionManager.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document onKeyDown={this.onKeyDown}>
  • {hideTitle && this.doc.type !== DocumentType.RTF ? - this.renderEmbeddedDocument(false) : + this.renderEmbeddedDocument(false, returnFalse) : this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader : this.renderTitleAsHeader, this._editTitle)}
  • ; -- cgit v1.2.3-70-g09d2 From dc3b329885785297b24f7f73e5d60dd56c370c7f Mon Sep 17 00:00:00 2001 From: bobzel Date: Sat, 13 Nov 2021 11:13:39 -0500 Subject: treeview title fixes - made it aformatted text box so clicking on it will select the document. fixed margins. fixed dragging over tree view to not highlight drop bullets in outline mode unless its a bullet point being moved. --- src/client/util/DragManager.ts | 2 ++ .../views/collections/CollectionTreeView.tsx | 30 ++++++++-------------- src/client/views/collections/TreeView.tsx | 8 +++--- 3 files changed, 17 insertions(+), 23 deletions(-) (limited to 'src/client/views/collections/TreeView.tsx') diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index f5704d2bf..ae3fa3170 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -335,8 +335,10 @@ export namespace DragManager { } export let docsBeingDragged: Doc[] = []; export let CanEmbed = false; + export let DocDragData: DocumentDragData | undefined; export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) { if (dragData.dropAction === "none") return; + DocDragData = dragData instanceof DocumentDragData ? dragData : undefined; const batch = UndoManager.StartBatch("dragging"); eles = eles.filter(e => e); CanEmbed = dragData.canEmbed || false; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index 6fb18d4c2..ea077ea40 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -7,7 +7,7 @@ import { Document, listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue } from '../../../Utils'; +import { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnOne } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; import { CurrentUserUtils } from '../../util/CurrentUserUtils'; import { DocumentManager } from '../../util/DocumentManager'; @@ -47,7 +47,8 @@ export class CollectionTreeView extends CollectionSubView = new Set(); // list of tree view items to monitor for height changes - private observer: any; // observer for mnonitoring tree view items. + private observer: any; // observer for monitoring tree view items. + private static expandViewLabelSize = 20; @computed get doc() { return this.props.Document; } @computed get dataDoc() { return this.props.DataDoc || this.doc; } @@ -187,36 +188,26 @@ export class CollectionTreeView extends CollectionSubView; } @@ -265,7 +256,7 @@ export class CollectionTreeView extends CollectionSubView this._titleRef = r} onKeyDown={e => { if (this.outlineMode) { @@ -333,10 +324,9 @@ export class CollectionTreeView extends CollectionSubView NumCast(this.doc._yMargin); documentTitleWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.panelWidth()); documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins); - titleTransform = () => this.props.ScreenToLocalTransform().translate(-NumCast(this.doc._xPadding, 10), -NumCast(this.doc._yPadding, 20)); truncateTitleWidth = () => this.treeViewtruncateTitleWidth; onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); - panelWidth = () => (this.props.PanelWidth() - 2 * this.marginX()) * (this.props.scaling?.() || 1); + panelWidth = () => Math.max(0, this.props.PanelWidth() - this.marginX() - CollectionTreeView.expandViewLabelSize) * (this.props.scaling?.() || 1); addAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.addDocument(doc, `${this.props.fieldKey}-annotations`) || false; remAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.removeDocument(doc, `${this.props.fieldKey}-annotations`) || false; diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index a266c301f..d8f984601 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -221,9 +221,11 @@ export class TreeView extends React.Component { const before = pt[1] < rect.top + rect.height / 2; const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length); this._header.current!.className = "treeView-header"; - if (inside) this._header.current!.className += " treeView-header-inside"; - else if (before) this._header.current!.className += " treeView-header-above"; - else if (!before) this._header.current!.className += " treeView-header-below"; + if (!this.props.treeView.outlineMode || DragManager.DocDragData?.treeViewDoc === this.props.treeView.rootDoc) { + if (inside) this._header.current!.className += " treeView-header-inside"; + else if (before) this._header.current!.className += " treeView-header-above"; + else if (!before) this._header.current!.className += " treeView-header-below"; + } e.stopPropagation(); } -- cgit v1.2.3-70-g09d2 From 49e9103721ecfd6d5c900c754f2d88362f64a3ab Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 2 Dec 2021 00:22:05 -0500 Subject: added shift erase stroke to delete full strokes. added shift drag end of stroke to scale uniformly. added ctrl+p,ctrl+e to switch between eraser/pen. added delete to menu options for tree view items. cleaned up a lot of ink code. --- src/client/views/GlobalKeyHandler.ts | 4 + src/client/views/InkControlPtHandles.tsx | 18 +- src/client/views/InkStrokeProperties.ts | 13 +- src/client/views/collections/TreeView.tsx | 13 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 219 ++++++++++----------- src/fields/InkField.ts | 16 +- 6 files changed, 136 insertions(+), 147 deletions(-) (limited to 'src/client/views/collections/TreeView.tsx') diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index d5e0ed962..1a4080d81 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -230,6 +230,10 @@ export class KeyManager { } } break; + case "e": CurrentUserUtils.SelectedTool = InkTool.Eraser; + break; + case "p": CurrentUserUtils.SelectedTool = InkTool.Pen; + break; case "o": const target = SelectionManager.Docs().lastElement(); target && CollectionDockingView.OpenFullScreen(target); diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx index 76ce73b0d..0996e75d4 100644 --- a/src/client/views/InkControlPtHandles.tsx +++ b/src/client/views/InkControlPtHandles.tsx @@ -191,16 +191,16 @@ export class InkEndPtHandles extends React.Component { setupMoveUpEvents(this, e, (e) => { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("stretch ink"); // compute stretch factor by finding scaling along axis between start and end points - const v1 = { x: p1().X - p2().X, y: p1().Y - p2().Y }; - const v2 = { x: e.clientX - p2().X, y: e.clientY - p2().Y }; - const v1len = Math.sqrt(v1.x * v1.x + v1.y * v1.y); - const v2len = Math.sqrt(v2.x * v2.x + v2.y * v2.y); + const v1 = { X: p1().X - p2().X, Y: p1().Y - p2().Y }; + const v2 = { X: e.clientX - p2().X, Y: e.clientY - p2().Y }; + const v1len = Math.sqrt(v1.X * v1.X + v1.Y * v1.Y); + const v2len = Math.sqrt(v2.X * v2.X + v2.Y * v2.Y); const scaling = v2len / v1len; - const v1n = { x: v1.x / v1len, y: v1.y / v1len }; - const v2n = { x: v2.x / v2len, y: v2.y / v2len }; - const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y); - InkStrokeProperties.Instance.stretchInk([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n); - InkStrokeProperties.Instance.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y }); + const v1n = { X: v1.X / v1len, Y: v1.Y / v1len }; + const v2n = { X: v2.X / v2len, Y: v2.Y / v2len }; + const angle = Math.acos(v1n.X * v2n.X + v1n.Y * v2n.Y) * Math.sign(v1.X * v2.Y - v2.X * v1.Y); + InkStrokeProperties.Instance.stretchInk([this.props.inkView], scaling, p2(), v1n, e.shiftKey); + InkStrokeProperties.Instance.rotateInk([this.props.inkView], angle, p2()); return false; }, action(() => { this.controlUndo?.end(); diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 9634e6e83..695bdcc5a 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -205,18 +205,17 @@ export class InkStrokeProperties { */ @undoBatch @action - stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: { x: number, y: number }, scrVec: { x: number, y: number }) => { + stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => { this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => { const ptFromScreen = view.ComponentView?.ptFromScreen; const ptToScreen = view.ComponentView?.ptToScreen; return !ptToScreen || !ptFromScreen ? ink : ink.map(ptToScreen).map(i => { - const pvec = { X: i.X - scrpt.x, Y: i.Y - scrpt.y }; - const svec = pvec.X * scrVec.x * scaling + pvec.Y * scrVec.y * scaling; - const ovec = -pvec.X * scrVec.y + pvec.Y * (scrVec.x); - const newscrpt = { X: scrpt.x + svec * scrVec.x - ovec * scrVec.y, Y: scrpt.y + svec * scrVec.y + ovec * scrVec.x }; - const newpt = ptFromScreen(newscrpt); - return newpt; + const pvec = { X: i.X - scrpt.X, Y: i.Y - scrpt.Y }; + const svec = pvec.X * scrVec.X * scaling + pvec.Y * scrVec.Y * scaling; + const ovec = -pvec.X * scrVec.Y * (scaleUniformly ? scaling : 1) + pvec.Y * scrVec.X * (scaleUniformly ? scaling : 1); + const newscrpt = { X: scrpt.X + svec * scrVec.X - ovec * scrVec.Y, Y: scrpt.Y + svec * scrVec.Y + ovec * scrVec.X }; + return ptFromScreen(newscrpt); }); }); } diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index d8f984601..eedb353e3 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -254,9 +254,7 @@ export class TreeView extends React.Component { TreeView._editTitleOnLoad = { id: folder[Id], parent: this.props.parentTreeView }; return this.props.addDocument(folder); } - deleteFolder = () => { - return this.props.removeDoc?.(this.doc); - } + deleteItem = () => this.props.removeDoc?.(this.doc); preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { const dragData = de.complete.docDragData; @@ -531,16 +529,16 @@ export class TreeView extends React.Component { } contextMenuItems = () => { const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "New Folder" }; - const deleteFolder = { script: ScriptField.MakeFunction(`scriptContext.deleteFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "Delete Folder" }; - const folderOp = this.childDocs?.length ? makeFolder : deleteFolder; + const deleteItem = { script: ScriptField.MakeFunction(`scriptContext.deleteItem()`, { scriptContext: "any" })!, icon: "folder-plus", label: "Delete" }; + const folderOp = this.childDocs?.length ? [makeFolder] : []; const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, icon: "copy", label: "Open Alias" }; const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: "eye", label: "Focus or Open" }; - return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? [folderOp] : + return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? folderOp : Doc.IsSystem(this.doc) ? [] : this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ? [openAlias, makeFolder] : this.doc.viewType === CollectionViewType.Docking ? [] : - [openAlias, focusDoc])]; + [deleteItem, openAlias, focusDoc])]; } childContextMenuItems = () => { const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []); @@ -784,6 +782,7 @@ export class TreeView extends React.Component { const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, "copy", undefined, false)); } + render() { TraceMobx(); const hideTitle = this.doc.treeViewHideHeader || this.props.treeView.outlineMode; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index ceee4051b..19c3bf745 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,11 +1,12 @@ -import { action, computed, IReactionDisposer, observable, reaction, runInAction, ObservableMap } from "mobx"; +import { Bezier } from "bezier-js"; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx"; import { observer } from "mobx-react"; import { computedFn } from "mobx-utils"; import { DateField } from "../../../../fields/DateField"; import { Doc, HeightSym, Opt, StrListCast, WidthSym } from "../../../../fields/Doc"; import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas"; import { Id } from "../../../../fields/FieldSymbols"; -import { InkData, InkField, InkTool, PointData, Intersection, Segment } from "../../../../fields/InkField"; +import { InkData, InkField, InkTool, PointData, Segment } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { ObjectField } from "../../../../fields/ObjectField"; import { RichTextField } from "../../../../fields/RichTextField"; @@ -27,14 +28,15 @@ import { InteractionUtils } from "../../../util/InteractionUtils"; import { LinkManager } from "../../../util/LinkManager"; import { SearchUtil } from "../../../util/SearchUtil"; import { SelectionManager } from "../../../util/SelectionManager"; +import { ColorScheme } from "../../../util/SettingsManager"; 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 { DocumentDecorations } from "../../DocumentDecorations"; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkWidth, SetActiveFillColor, SetActiveInkColor } from "../../InkingStroke"; +import { GestureOverlay } from "../../GestureOverlay"; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from "../../InkingStroke"; import { LightboxView } from "../../LightboxView"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView"; @@ -50,10 +52,6 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); -import { ColorScheme } from "../../../util/SettingsManager"; -import { Bezier } from "bezier-js"; -import { GestureOverlay } from "../../GestureOverlay"; -import { constants } from "perf_hooks"; export const panZoomSchema = createSchema({ _panX: "number", @@ -115,8 +113,6 @@ export class CollectionFreeFormView extends CollectionSubView(); @observable _marqueeRef = React.createRef(); @@ -439,28 +435,29 @@ export class CollectionFreeFormView extends CollectionSubView { - if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || - ([InkTool.Pen, InkTool.Highlighter].includes(CurrentUserUtils.SelectedTool))) { - return; - } - this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); + this._downX = this._lastX = e.pageX; + this._downY = this._lastY = e.pageY; if (e.button === 0 && !e.altKey && !e.ctrlKey && this.props.isContentActive(true)) { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - document.addEventListener("pointermove", this.onPointerMove); - document.addEventListener("pointerup", this.onPointerUp); - // if not using a pen and in no ink mode - if (CurrentUserUtils.SelectedTool === InkTool.None) { - this._downX = this._lastX = e.pageX; - this._downY = this._lastY = e.pageY; - } - // eraser plus anything else mode - else { - this._batch = UndoManager.StartBatch("collectionErase"); - this._prevPoint = { X: e.clientX, Y: e.clientY }; - e.stopPropagation(); - e.preventDefault(); - } + if (!e.nativeEvent.cancelBubble && + !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)) + switch (CurrentUserUtils.SelectedTool) { + case InkTool.Highlighter: + 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: + document.addEventListener("pointermove", this.onEraserMove); + document.addEventListener("pointerup", this.onEraserUp); + this._batch = UndoManager.StartBatch("collectionErase"); + e.stopPropagation(); + e.preventDefault(); + break; + case InkTool.None: + this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); + document.addEventListener("pointermove", this.onPointerMove); + document.addEventListener("pointerup", this.onPointerUp); + break; + } } } @@ -600,6 +597,16 @@ export class CollectionFreeFormView extends CollectionSubView { + if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { + document.removeEventListener("pointermove", this.onEraserMove); + document.removeEventListener("pointerup", this.onEraserUp); + this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc)); + this._deleteList = []; + this._batch?.end(); + } + } @action onPointerUp = (e: PointerEvent): void => { @@ -608,12 +615,6 @@ export class CollectionFreeFormView extends CollectionSubView ink.props.removeDocument?.(ink.rootDoc)); - this._prevPoint = this._currPoint = { X: -1, Y: -1 }; - this._deleteList = []; - this._batch?.end(); - } } } @@ -638,96 +639,82 @@ export class CollectionFreeFormView extends CollectionSubView { - if (this.props.Document._isGroup) return; // groups don't pan when dragged -- instead let the event go through to allow the group itself to drag - if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return; - if (CurrentUserUtils.SelectedTool !== InkTool.None) { - this._currPoint = { X: e.clientX, Y: e.clientY }; - // Erasing ink strokes if intersections occur. - this.eraseInkStrokes(this.getEraserIntersections()); - } - if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { - if (this.props.isContentActive(true)) e.stopPropagation(); - } else if (!e.cancelBubble) { - if (CurrentUserUtils.SelectedTool === InkTool.None) { - if (this.tryDragCluster(e, this._hitCluster)) { - document.removeEventListener("pointermove", this.onPointerMove); - document.removeEventListener("pointerup", this.onPointerUp); - } - else this.pan(e); - } - e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers - e.preventDefault(); - } - } - /** - * Iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments, + * Erases strokes by intersecting them with an invisible "eraser stroke". + * By default this iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments, * and deletes the original stroke. - * @param eraserIntersections The intersections made by the eraser. + * However, if Shift is held, then no segmentation is done -- instead any intersected stroke is deleted in its entirety. */ - eraseInkStrokes = (eraserIntersections: Intersection[]) => { - eraserIntersections.forEach(intersect => { - const ink = intersect.ink; - if (ink && !this._deleteList.includes(ink)) { - this._deleteList.push(ink); - SetActiveInkWidth(StrCast(ink.rootDoc.strokeWidth?.toString()) || "1"); - SetActiveInkColor(StrCast(ink.rootDoc.color?.toString()) || "black"); + @action + onEraserMove = (e: PointerEvent) => { + const currPoint = { X: e.clientX, Y: e.clientY }; + this.getEraserIntersections({ X: this._lastX, Y: this._lastY }, currPoint).forEach(intersect => { + if (!this._deleteList.includes(intersect.inkView)) { + this._deleteList.push(intersect.inkView); + SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.strokeWidth?.toString()) || "1"); + SetActiveInkColor(StrCast(intersect.inkView.rootDoc.color?.toString()) || "black"); // create a new curve by appending all curves of the current segment together in order to render a single new stroke. - this.segmentInkStroke(ink, intersect.t ?? 0).forEach(segment => + !e.shiftKey && this.segmentInkStroke(intersect.inkView, intersect.t).forEach(segment => GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, segment.reduce((data, curve) => [...data, ...curve.points - .map(p => ink.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 }) + .map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 }) ], [] as PointData[]))); // Lower ink opacity to give the user a visual indicator of deletion. - ink.layoutDoc.opacity = 0.5; + intersect.inkView.layoutDoc.opacity = 0.5; } }); + this._lastX = currPoint.X; + this._lastY = currPoint.Y; + + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers + e.preventDefault(); + } + + @action + onPointerMove = (e: PointerEvent): void => { + if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return; + if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { + if (this.props.isContentActive(true)) e.stopPropagation(); + } else if (!e.cancelBubble) { + if (this.tryDragCluster(e, this._hitCluster)) { + document.removeEventListener("pointermove", this.onPointerMove); + document.removeEventListener("pointerup", this.onPointerUp); + } + else this.pan(e); + e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers + e.preventDefault(); + } } /** * Determines if the Eraser tool has intersected with an ink stroke in the current freeform collection. - * @returns A dictionary mapping the t-value intersection of the eraser with the corresponding ink DocumentView. + * @returns an array of tuples containing the intersected ink DocumentView and the t-value where it was intersected */ - getEraserIntersections = (): Intersection[] => { - const intersections: Intersection[] = []; - this.childDocs - .filter(doc => doc.type === DocumentType.INK) - .forEach(doc => { - const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView); - const inkStroke = inkView?.ComponentView as InkingStroke; - const { inkData } = inkStroke?.inkScaledData(); + getEraserIntersections = (lastPoint: { X: number, Y: number }, currPoint: { X: number, Y: number }) => { + const eraserMin = { X: Math.min(lastPoint.X, currPoint.X), Y: Math.min(lastPoint.Y, currPoint.Y) }; + const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) }; + return this.childDocs + .map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)) + .filter(inkView => inkView?.ComponentView instanceof InkingStroke) + .map(inkView => ({ inkViewBounds: inkView!.getBounds(), inkStroke: inkView!.ComponentView as InkingStroke, inkView: inkView! })) + .filter(({ inkViewBounds }) => inkViewBounds && // bounding box of eraser segment and ink stroke overlap + eraserMin.X <= inkViewBounds.right && eraserMin.Y <= inkViewBounds.bottom && + eraserMax.X >= inkViewBounds.left && eraserMax.Y >= inkViewBounds.top) + .reduce((intersections, { inkStroke, inkView }) => { + const { inkData } = inkStroke.inkScaledData(); + // Convert from screen space to ink space for the intersection. + const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint); + const currPointInkSpace = inkStroke.ptFromScreen(currPoint); for (var i = 0; i < inkData.length - 3; i += 4) { - const array = inkData.slice(i, i + 4); - // Converting from screen space to ink space for the intersection. - const prevPointInkSpace = inkStroke?.ptFromScreen?.(this._prevPoint); - const currPointInkSpace = inkStroke?.ptFromScreen?.(this._currPoint); - if (prevPointInkSpace && currPointInkSpace) { - const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))); - const intersects = curve.intersects({ - p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, - p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y } - }); - if (inkView && intersects) { - for (const val of intersects) { - // Casting t-value from type: (string | number) to number for comparisons. - const t = +(Number(val) + Math.floor(i / 4)).toString(); // add start of curve segment to convert from local t value to t value along complete curve - var unique: boolean = true; - // Ensuring there are no duplicate intersections in the list returned. - for (const prevIntersect of intersections) { - if (prevIntersect.t === t) { - unique = false; - break; - } - } - if (unique) intersections.push({ t: +t.toString(), ink: inkView, curve: curve }); - } - } - } + const intersects = Array.from(new Set(InkField.Segment(inkData, i).intersects({ // compute all unique intersections + p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, + p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y } + }) as (number | string)[])); // convert to more manageable union array type + // return tuples of the inkingStroke intersected, and the t value of the intersection + intersections.push(...intersects.map(t => ({ inkView, t: (+t) + Math.floor(i / 4) })));// convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve } - }); - return intersections; + return intersections; + }, [] as { t: number, inkView: DocumentView }[]); } /** @@ -746,23 +733,23 @@ export class CollectionFreeFormView extends CollectionSubView ({ x: p.X, y: p.Y }))); + const inkSegment = InkField.Segment(inkData, i); // Getting all t-value intersections of the current curve with all other curves. - const tVals = this.getInkIntersections(i, ink, curve).sort(); + const tVals = this.getInkIntersections(i, ink, inkSegment).sort(); if (tVals.length) { tVals.forEach((t, index) => { const docCurveTVal = t + Math.floor(i / 4); if (excludeT < startSegmentT || excludeT > docCurveTVal) { const localStartTVal = startSegmentT - Math.floor(i / 4); - segment.push(curve.split(localStartTVal < 0 ? 0 : localStartTVal, t)); + segment.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t)); segment.length && segments.push(segment); } // start a new segment from the intersection t value - segment = tVals.length - 1 === index ? [curve.split(t).right] : []; + segment = tVals.length - 1 === index ? [inkSegment.split(t).right] : []; startSegmentT = docCurveTVal; }); } else { - segment.push(curve); + segment.push(inkSegment); } } if (excludeT < startSegmentT || excludeT > (inkData.length / 4)) { @@ -786,7 +773,7 @@ export class CollectionFreeFormView extends CollectionSubView doc.type === DocumentType.INK) .forEach(doc => { const otherInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)?.ComponentView as InkingStroke; - const { inkData: otherInkData } = otherInk.inkScaledData(); + 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) { diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index f61313674..560cf3d63 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -3,7 +3,6 @@ import { Scripting } from "../client/util/Scripting"; import { Deserializable } from "../client/util/SerializationHelper"; import { Copy, ToScriptString, ToString } from "./FieldSymbols"; import { ObjectField } from "./ObjectField"; -import { DocumentView } from "../client/views/nodes/DocumentView"; import { Bezier } from "bezier-js"; // Helps keep track of the current ink tool in use. @@ -22,13 +21,6 @@ export interface PointData { Y: number; } -export interface Intersection { - t?: number; - ink?: DocumentView; - curve?: Bezier; - index?: number; -} - export type Segment = Array; // Defines an ink as an array of points. @@ -78,6 +70,14 @@ export class InkField extends ObjectField { this.inkData = data; } + /** + * Extacts a simple segment from a compound Bezier curve + * @param segIndex the start index of the simple bezier segment to extact (eg., 0, 4, 8, ...) + */ + public static Segment(inkData: InkData, segIndex: number) { + return new Bezier(inkData.slice(segIndex, segIndex + 4).map(pt => ({ x: pt.X, y: pt.Y }))); + } + [Copy]() { return new InkField(this.inkData); } -- cgit v1.2.3-70-g09d2