diff options
Diffstat (limited to 'src/client/views/collections')
15 files changed, 382 insertions, 428 deletions
diff --git a/src/client/views/collections/CollectionCalendarView.tsx b/src/client/views/collections/CollectionCalendarView.tsx deleted file mode 100644 index 0ea9f8ebc..000000000 --- a/src/client/views/collections/CollectionCalendarView.tsx +++ /dev/null @@ -1,99 +0,0 @@ -import { computed, makeObservable } from 'mobx'; -import { observer } from 'mobx-react'; -import * as React from 'react'; -import { emptyFunction } from '../../../Utils'; -import { dateRangeStrToDates, returnTrue } from '../../../ClientUtils'; -import { Doc, DocListCast } from '../../../fields/Doc'; -import { StrCast } from '../../../fields/Types'; -import { CollectionStackingView } from './CollectionStackingView'; -import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; - -@observer -export class CollectionCalendarView extends CollectionSubView() { - constructor(props: SubCollectionViewProps) { - super(props); - makeObservable(this); - } - - componentDidMount(): void {} - - componentWillUnmount(): void {} - - @computed get allCalendars() { - return this.childDocs; // returns a list of docs (i.e. calendars) - } - - removeCalendar = () => {}; - - addCalendar = (/* doc: Doc | Doc[], annotationKey?: string | undefined */): boolean => - // bring up calendar modal with option to create a calendar - true; - - _stackRef = React.createRef<CollectionStackingView>(); - - panelHeight = () => this._props.PanelHeight() - 40; // this should be the height of the stacking view. For now, it's the hieight of the calendar view minus 40 to allow for a title - - // most recent calendar should come first - sortByMostRecentDate = (calendarA: Doc, calendarB: Doc) => { - const aDateRangeStr = StrCast(DocListCast(calendarA.data).lastElement()?.date_range); - const bDateRangeStr = StrCast(DocListCast(calendarB.data).lastElement()?.date_range); - - const { start: aFromDate, end: aToDate } = dateRangeStrToDates(aDateRangeStr); - const { start: bFromDate, end: bToDate } = dateRangeStrToDates(bDateRangeStr); - - if (aFromDate > bFromDate) { - return -1; // a comes first - } - if (aFromDate < bFromDate) { - return 1; // b comes first - } - // start dates are the same - if (aToDate > bToDate) { - return -1; // a comes first - } - if (aToDate < bToDate) { - return 1; // b comes first - } - return 0; // same start and end dates - }; - - screenToLocalTransform = () => - this._props - .ScreenToLocalTransform() - .translate(Doc.NativeWidth(this.Document), 0) - .scale(this._props.NativeDimScaling?.() || 1); - - get calendarsKey() { - return this._props.fieldKey; - } - - render() { - return ( - <div className="collectionCalendarView"> - <CollectionStackingView - // eslint-disable-next-line react/jsx-props-no-spreading - {...this._props} - setContentViewBox={emptyFunction} - ref={this._stackRef} - PanelHeight={this.panelHeight} - PanelWidth={this._props.PanelWidth} - sortFunc={this.sortByMostRecentDate} - setHeight={undefined} - isAnnotationOverlay={false} - // select={emptyFunction} What does this mean? - isAnyChildContentActive={returnTrue} // ?? - dontCenter="y" - // childDocumentsActive={} - // whenChildContentsActiveChanged={} - childHideDecorationTitle={false} - removeDocument={this.removeDocument} // will calendar automatically be removed from myCalendars - moveDocument={this.moveDocument} - addDocument={this.addCalendar} - ScreenToLocalTransform={this.screenToLocalTransform} - renderDepth={this._props.renderDepth + 1} - fieldKey={this.calendarsKey} - /> - </div> - ); - } -} diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx index e5a6ebc7f..92c69c3cf 100644 --- a/src/client/views/collections/CollectionCardDeckView.tsx +++ b/src/client/views/collections/CollectionCardDeckView.tsx @@ -1,8 +1,7 @@ -import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, trace } from 'mobx'; +import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, DashColor, returnFalse, returnZero } from '../../../ClientUtils'; -import { emptyFunction } from '../../../Utils'; import { Doc } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; @@ -13,7 +12,6 @@ import { gptImageLabel } from '../../apis/gpt/GPT'; import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { dropActionType } from '../../util/DropActionTypes'; -import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; import { undoable } from '../../util/UndoManager'; @@ -23,12 +21,13 @@ import { DocumentView } from '../nodes/DocumentView'; import { GPTPopup, GPTPopupMode } from '../pdf/GPTPopup/GPTPopup'; import './CollectionCardDeckView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; +import { computedFn } from 'mobx-utils'; +import { DocumentDecorations } from '../DocumentDecorations'; enum cardSortings { Time = 'time', Type = 'type', Color = 'color', - Custom = 'custom', Chat = 'chat', Tag = 'tag', None = '', @@ -46,14 +45,13 @@ export class CollectionCardView extends CollectionSubView() { private _dropDisposer?: DragManager.DragDropDisposer; private _disposers: { [key: string]: IReactionDisposer } = {}; private _textToDoc = new Map<string, Doc>(); - private _dropped = false; // indicate when a card doc has just moved; + private _dropped = false; // set when a card doc has just moved and the drop method has been called - prevents the pointerUp method from hiding doc decorations (which needs to be done when clicking on a card to animate it to front/center) @observable _forceChildXf = 0; @observable _hoveredNodeIndex = -1; @observable _docRefs = new ObservableMap<Doc, DocumentView>(); @observable _maxRowCount = 10; @observable _docDraggedIndex: number = -1; - @observable overIndex: number = -1; static imageUrlToBase64 = async (imageUrl: string): Promise<string> => { try { @@ -101,7 +99,6 @@ export class CollectionCardView extends CollectionSubView() { componentDidMount() { this._props.setContentViewBox?.(this); - // Reaction to cardSort changes this._disposers.sort = reaction( () => GPTPopup.Instance.visible, isVis => { @@ -135,11 +132,10 @@ export class CollectionCardView extends CollectionSubView() { } /** - * The child documents to be rendered-- either all of them except the Links or the docs in the currently active - * custom group + * The child documents to be rendered-- everything other than ink/link docs (which are marks as being svg's) */ @computed get childDocsWithoutLinks() { - return this.childDocs.filter(l => l.type !== DocumentType.LINK); + return this.childDocs.filter(l => !l.layout_isSvg); } /** @@ -155,8 +151,7 @@ export class CollectionCardView extends CollectionSubView() { */ quizMode = () => { const randomIndex = Math.floor(Math.random() * this.childDocs.length); - SelectionManager.DeselectAll(); - DocumentView.SelectView(DocumentView.getDocumentView(this.childDocs[randomIndex]), false); + DocumentView.getDocumentView(this.childDocs[randomIndex])?.select(false); }; /** @@ -168,29 +163,15 @@ export class CollectionCardView extends CollectionSubView() { @action setHoveredNodeIndex = (index: number) => { - if (!DocumentView.SelectedDocs().includes(this.childDocs[index])) { - this._hoveredNodeIndex = index; - } + if (!SnappingManager.IsDragging) this._hoveredNodeIndex = index; }; - /** - * Translates the hovered node to the center of the screen - * @param index - * @returns - */ - translateHover = (index: number) => (this._hoveredNodeIndex === index && !DocumentView.SelectedDocs().includes(this.childDocs[index]) ? -50 : 0); - - isSelected = (index: number) => DocumentView.SelectedDocs().includes(this.childDocs[index]); - - /** - * Returns all the documents except the one that's currently selected - */ - inactiveDocs = () => this.childDocsWithoutLinks.filter(d => !DocumentView.SelectedDocs().includes(d)); + isSelected = (doc: Doc) => this._docRefs.get(doc)?.IsSelected; childPanelWidth = () => NumCast(this.layoutDoc.childPanelWidth, this._props.PanelWidth() / 2); childPanelHeight = () => this._props.PanelHeight() * this.fitContentScale; onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); - isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive(); - isChildContentActive = () => !!this.isContentActive(); + isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this.isAnyChildContentActive(); + isAnyChildContentActive = this._props.isAnyChildContentActive; /** * Returns the degree to rotate a card dependind on the amount of cards in their row and their index in said row @@ -202,13 +183,14 @@ export class CollectionCardView extends CollectionSubView() { if (amCards == 1) return 0; const possRotate = -30 + index * (30 / ((amCards - (amCards % 2)) / 2)); - const stepMag = Math.abs(-30 + (amCards / 2 - 1) * (30 / ((amCards - (amCards % 2)) / 2))); - - if (amCards % 2 === 0 && possRotate === 0) { - return possRotate + Math.abs(-30 + (index - 1) * (30 / (amCards / 2))); - } - if (amCards % 2 === 0 && index > (amCards + 1) / 2) { - return possRotate + stepMag; + if (amCards % 2 === 0) { + if (possRotate === 0) { + return possRotate + Math.abs(-30 + (index - 1) * (30 / (amCards / 2))); + } + if (index > (amCards + 1) / 2) { + const stepMag = Math.abs(-30 + (amCards / 2 - 1) * (30 / ((amCards - (amCards % 2)) / 2))); + return possRotate + stepMag; + } } return possRotate; @@ -274,22 +256,13 @@ export class CollectionCardView extends CollectionSubView() { /** * Checks to see if a card is being dragged and calls the appropriate methods if so - * @param e the current pointer event */ - @action onPointerMove = (x: number, y: number) => { this._docDraggedIndex = DragManager.docsBeingDragged.length ? this.findCardDropIndex(x, y) : -1; }; /** - * Handles external drop of images/PDFs etc from outside Dash. - */ - onExternalDrop = async (e: React.DragEvent): Promise<void> => { - super.onExternalDrop(e, {}); - }; - - /** * Resets all the doc dragging vairables once a card is dropped * @param e * @param de drop event @@ -326,7 +299,7 @@ export class CollectionCardView extends CollectionSubView() { } /** - * Used to determine how to sort cards based on tags. The lestmost tags are given lower values while cards to the right are + * Used to determine how to sort cards based on tags. The leftmost tags are given lower values while cards to the right are * given higher values. Decimals are used to determine placement for cards with multiple tags * @param doc the doc whose value is being determined * @returns its value based on its tags @@ -352,24 +325,14 @@ export class CollectionCardView extends CollectionSubView() { docs.sort((docA, docB) => { const [typeA, typeB] = (() => { switch (sortType) { - case cardSortings.Time: - return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()]; - case cardSortings.Color: { - const d1 = DashColor(StrCast(docA.backgroundColor)); - const d2 = DashColor(StrCast(docB.backgroundColor)); - return [d1.hsv().hue(), d2.hsv().hue()]; - } - case cardSortings.Tag: - return [this.tagValue(docA) ?? 9999, this.tagValue(docB) ?? 9999]; - case cardSortings.Chat: - return [NumCast(docA.chatIndex) ?? 9999, NumCast(docB.chatIndex) ?? 9999]; - default: - return [StrCast(docA.type), StrCast(docB.type)]; + default: + case cardSortings.Type: return [StrCast(docA.type), StrCast(docB.type)]; + case cardSortings.Chat: return [NumCast(docA.chatIndex, 9999), NumCast(docB.chatIndex,9999)]; + case cardSortings.Time: return [DateCast(docA.author_date)?.date ?? Date.now(), DateCast(docB.author_date)?.date ?? Date.now()]; + case cardSortings.Color:return [DashColor(StrCast(docA.backgroundColor)).hsv().hue(), DashColor(StrCast(docB.backgroundColor)).hsv().hue()]; } - })(); - - const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0; - return isDesc ? out : -out; + })(); //prettier-ignore + return (typeA < typeB ? -1 : typeA > typeB ? 1 : 0) * (isDesc ? 1 : -1); }); if (dragIndex !== -1) { const draggedDoc = DragManager.docsBeingDragged[0]; @@ -382,6 +345,15 @@ export class CollectionCardView extends CollectionSubView() { return docs; }; + isChildContentActive = () => + this._props.isContentActive?.() === false + ? false + : this._props.isDocumentActive?.() && (this._props.childDocumentsActive?.() || BoolCast(this.Document.childDocumentsActive)) + ? true + : this._props.childDocumentsActive?.() === false || this.Document.childDocumentsActive === false + ? false + : undefined; + displayDoc = (doc: Doc, screenToLocalTransform: () => Transform) => ( <DocumentView {...this._props} @@ -396,13 +368,14 @@ export class CollectionCardView extends CollectionSubView() { LayoutTemplateString={this._props.childLayoutString} containerViewPath={this.childContainerViewPath} ScreenToLocalTransform={screenToLocalTransform} // makes sure the box wrapper thing is in the right spot - isContentActive={emptyFunction} isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive} PanelWidth={this.childPanelWidth} PanelHeight={this.childPanelHeight} dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice. dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType} - showTags={true} + showTags={BoolCast(this.layoutDoc.showChildTags)} + whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged} + isContentActive={this.isChildContentActive} dontHideOnDrag /> ); @@ -416,13 +389,11 @@ export class CollectionCardView extends CollectionSubView() { if (this.sortedDocs.length < this._maxRowCount) { return this.sortedDocs.length; } - // 13 - 3 = 10 const totalCards = this.sortedDocs.length; // if 9 or less if (index < totalCards - (totalCards % this._maxRowCount)) { return this._maxRowCount; } - // (3) return totalCards % this._maxRowCount; }; /** @@ -441,19 +412,18 @@ export class CollectionCardView extends CollectionSubView() { translateOverflowX = (realIndex: number, calcRowCards: number) => (realIndex < this._maxRowCount ? 0 : (this._maxRowCount - calcRowCards) * (this.childPanelWidth() / 2)); /** - * Determines how far to translate a card in the y direction depending on its index, whether or not its being hovered, or if it's selected - * @param isHovered - * @param isSelected - * @param realIndex - * @param amCards - * @param calcRowIndex - * @returns + * Determines how far to translate a card in the y direction depending on its index and if it's selected + * @param isActive whether the card is focused for interaction + * @param realIndex index of card from start of deck + * @param amCards ?? + * @param calcRowIndex index of card from start of row + * @returns Y translation of card */ - calculateTranslateY = (isHovered: boolean, isSelected: boolean, realIndex: number, amCards: number, calcRowIndex: number) => { + calculateTranslateY = (isActive: boolean, realIndex: number, amCards: number, calcRowIndex: number) => { const rowHeight = (this._props.PanelHeight() * this.fitContentScale) / this.numRows; const rowIndex = Math.trunc(realIndex / this._maxRowCount); const rowToCenterShift = this.numRows / 2 - rowIndex; - if (isSelected) return rowToCenterShift * rowHeight - rowHeight / 2; + if (isActive) return rowToCenterShift * rowHeight - rowHeight / 2; if (amCards == 1) return 50 * this.fitContentScale; return this.translateY(amCards, calcRowIndex, realIndex); }; @@ -576,15 +546,43 @@ export class CollectionCardView extends CollectionSubView() { await this.childPairStringListAndUpdateSortDesc(); }; + childScreenToLocal = computedFn((doc: Doc, index: number, calcRowIndex: number, isSelected: boolean, amCards: number) => () => { + // need to explicitly trigger an invalidation since we're reading everything from the Dom + this._forceChildXf; + this._props.ScreenToLocalTransform(); + + const dref = this._docRefs.get(doc); + const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv); + if (!scale) return new Transform(0, 0, 0); + + return new Transform(-translateX + (dref?.centeringX || 0) * scale, + -translateY + (dref?.centeringY || 0) * scale, 1) + .scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); // prettier-ignore + }); + + cardPointerUp = action((doc: Doc) => { + // if a card doc has just moved, or a card is selected and in front, then ignore this event + if (this.isSelected(doc) || this._dropped) { + this._dropped = false; + } else { + // otherwise, turn off documentDecorations becase we're in a selection transition and want to avoid artifacts. + // Turn them back on when the animation has completed and the render and backend structures are in synch + SnappingManager.SetHideDecorations(true); + setTimeout( + action(() => { + SnappingManager.SetHideDecorations(false); + this._forceChildXf++; + }), + 1000 + ); + } + }); + /** * Actually renders all the cards */ @computed get renderCards() { - const sortedDocs = this.sortedDocs; - const anySelected = this.childDocs.some(doc => DocumentView.SelectedDocs().includes(doc)); - const isEmpty = this.childDocsWithoutLinks.length === 0; - - if (isEmpty) { + if (!this.childDocsWithoutLinks.length) { return ( <span className="no-card-span" style={{ width: ` ${this._props.PanelWidth()}px`, height: ` ${this._props.PanelHeight()}px` }}> Sorry ! There are no cards in this group @@ -593,29 +591,17 @@ export class CollectionCardView extends CollectionSubView() { } // Map sorted documents to their rendered components - return sortedDocs.map((doc, index) => { - const realIndex = sortedDocs.indexOf(doc); - const calcRowIndex = this.overflowIndexCalc(realIndex); - const amCards = this.overflowAmCardsCalc(realIndex); + return this.sortedDocs.map((doc, index) => { + const calcRowIndex = this.overflowIndexCalc(index); + const amCards = this.overflowAmCardsCalc(index); const view = DocumentView.getDocumentView(doc, this.DocumentView?.()); - const isSelected = view?.ComponentView?.isAnyChildContentActive?.() || view?.IsSelected ? true : false; - - const childScreenToLocal = () => { - // need to explicitly trigger an invalidation since we're reading everything from the Dom - this._forceChildXf; - this._props.ScreenToLocalTransform(); - - const dref = this._docRefs.get(doc); - const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv); - return new Transform(-translateX + (dref?.centeringX || 0) * scale, - -translateY + (dref?.centeringY || 0) * scale, 1) - .scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); // prettier-ignore - }; + + const childScreenToLocal = this.childScreenToLocal(doc, index, calcRowIndex, !!view?.IsContentActive, amCards); const translateIfSelected = () => { const indexInRow = index % this._maxRowCount; const rowIndex = Math.trunc(index / this._maxRowCount); - const rowCenterIndex = Math.min(this._maxRowCount, sortedDocs.length - rowIndex * this._maxRowCount) / 2; + const rowCenterIndex = Math.min(this._maxRowCount, this.sortedDocs.length - rowIndex * this._maxRowCount) / 2; return (rowCenterIndex - indexInRow) * 100 - 50; }; const aspect = NumCast(doc.height) / NumCast(doc.width, 1); @@ -625,33 +611,18 @@ export class CollectionCardView extends CollectionSubView() { return ( <div key={doc[Id]} - className={`card-item${isSelected ? '-active' : anySelected ? '-inactive' : ''}`} - onPointerUp={action(() => { - // if a card doc has just moved, or a card is selected and in front, then ignore this event - if (DocumentView.SelectedDocs().includes(doc) || this._dropped) { - this._dropped = false; - } else { - // otherwise, turn off documentDecorations becase we're in a selection transition and want to avoid artifacts. - // Turn them back on when the animation has completed and the render and backend structures are in synch - SnappingManager.SetIsResizing(doc[Id]); - setTimeout( - action(() => { - SnappingManager.SetIsResizing(undefined); - this._forceChildXf++; - }), - 600 - ); - } - })} + className={`card-item${view?.IsContentActive ? '-active' : this.isAnyChildContentActive() ? '-inactive' : ''}`} + onPointerUp={() => this.cardPointerUp(doc)} style={{ width: this.childPanelWidth(), height: 'max-content', - transform: `translateY(${this.calculateTranslateY(this._hoveredNodeIndex === index, isSelected, realIndex, amCards, calcRowIndex)}px) - translateX(calc(${isSelected ? translateIfSelected() : 0}% + ${this.translateOverflowX(realIndex, amCards)}px)) - rotate(${!isSelected ? this.rotate(amCards, calcRowIndex) : 0}deg) - scale(${isSelected ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.05 : 1})`, + transform: `translateY(${this.calculateTranslateY(!!view?.IsContentActive, index, amCards, calcRowIndex)}px) + translateX(calc(${view?.IsContentActive ? translateIfSelected() : 0}% + ${this.translateOverflowX(index, amCards)}px)) + rotate(${!view?.IsContentActive ? this.rotate(amCards, calcRowIndex) : 0}deg) + scale(${view?.IsContentActive ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.1 : 1})`, }} // prettier-ignore - onPointerEnter={() => !SnappingManager.IsDragging && this.setHoveredNodeIndex(index)}> + onPointerEnter={() => this.setHoveredNodeIndex(index)} + onPointerLeave={() => this.setHoveredNodeIndex(-1)}> {this.displayDoc(doc, childScreenToLocal)} </div> ); @@ -678,8 +649,7 @@ export class CollectionCardView extends CollectionSubView() { ...(!isEmpty && { transform: `scale(${1 / this.fitContentScale})` }), ...(!isEmpty && { height: `${100 * this.fitContentScale}%` }), gridAutoRows: `${100 / this.numRows}%`, - }} - onMouseLeave={() => this.setHoveredNodeIndex(-1)}> + }}> {this.renderCards} </div> </div> diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index 139aebb02..c5da8e037 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -16,7 +16,7 @@ import './CollectionCarousel3DView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import { Transform } from '../../util/Transform'; -// eslint-disable-next-line @typescript-eslint/no-var-requires +// eslint-disable-next-line @typescript-eslint/no-require-imports const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss'); @observer @@ -89,14 +89,13 @@ export class CollectionCarousel3DView extends CollectionSubView() { const currentIndex = NumCast(this.layoutDoc._carousel_index); const displayDoc = (childPair: { layout: Doc; data: Doc }, dxf: () => Transform) => ( <DocumentView - // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} Document={childPair.layout} TemplateDataDocument={childPair.data} // suppressSetHeight={true} NativeWidth={returnZero} NativeHeight={returnZero} - fitWidth={undefined} + fitWidth={this._props.childLayoutFitWidth} containerViewPath={this.childContainerViewPath} onDoubleClickScript={this.onChildDoubleClick} renderDepth={this._props.renderDepth + 1} diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 6ea263215..8b3a699ed 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -3,18 +3,18 @@ import { IReactionDisposer, action, computed, makeObservable, observable, reacti import { observer } from 'mobx-react'; import * as React from 'react'; import { StopEvent, returnOne, returnZero } from '../../../ClientUtils'; -import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; import { BoolCast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { ContextMenu } from '../ContextMenu'; import { StyleProp } from '../StyleProp'; +import { TagItem } from '../TagsView'; import { DocumentView } from '../nodes/DocumentView'; import { FieldViewProps } from '../nodes/FieldView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; import './CollectionCarouselView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; -import { TagItem } from '../TagsView'; enum cardMode { PRACTICE = 'practice', @@ -164,8 +164,8 @@ export class CollectionCarouselView extends CollectionSubView() { Document={doc} NativeWidth={returnZero} NativeHeight={returnZero} - fitWidth={undefined} - showTags={true} + fitWidth={this._props.childLayoutFitWidth} + showTags={BoolCast(this.layoutDoc.showChildTags)} containerViewPath={this.childContainerViewPath} setContentViewBox={undefined} ScreenToLocalTransform={this.childScreenToLocalXf} @@ -178,6 +178,7 @@ export class CollectionCarouselView extends CollectionSubView() { LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} TemplateDataDocument={DocCast(Doc.Layout(doc).resolvedDataDoc)} + xPadding={35} PanelHeight={this.panelHeight} /> ); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index 028133a6e..e1786d2c9 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -31,17 +31,17 @@ import { ScriptingRepl } from '../ScriptingRepl'; import { UndoStack } from '../UndoStack'; import './CollectionDockingView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; -import { TabHTMLElement } from './TabDocView'; +import { TabDocView, TabHTMLElement } from './TabDocView'; @observer export class CollectionDockingView extends CollectionSubView() { - static tabClass: unknown = null; + static tabClass?: typeof TabDocView; /** * Initialize by assigning the add split method to DocumentView and by * configuring golden layout to render its documents using the specified React component * @param ele - typically would be set to TabDocView */ - public static Init(ele: unknown) { + public static Init(ele: typeof TabDocView) { this.tabClass = ele; DocumentView.addSplit = CollectionDockingView.AddSplit; } @@ -203,7 +203,6 @@ export class CollectionDockingView extends CollectionSubView() { } else if (instance._goldenLayout.root.contentItems[0].isRow) { // if row switch (pullSide) { - // eslint-disable-next-line default-case-last default: case OpenWhereMod.none: case OpenWhereMod.right: @@ -299,7 +298,7 @@ export class CollectionDockingView extends CollectionSubView() { this._goldenLayout.unbind('tabCreated', this.tabCreated); this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); this._goldenLayout.unbind('stackCreated', this.stackCreated); - } catch (e) { + } catch { /* empty */ } this.tabMap.clear(); @@ -380,7 +379,7 @@ export class CollectionDockingView extends CollectionSubView() { try { this._goldenLayout.unbind('stackCreated', this.stackCreated); this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed); - } catch (e) { + } catch { /* empty */ } this._goldenLayout?.destroy(); @@ -507,8 +506,8 @@ export class CollectionDockingView extends CollectionSubView() { dashboardDoc.myOverlayDocs = new List<Doc>(); dashboardDoc.myPublishedDocs = new List<Doc>(); - DashboardView.SetupDashboardTrails(dashboardDoc); - DashboardView.SetupDashboardCalendars(dashboardDoc); // Zaul TODO: needed? + DashboardView.SetupDashboardTrails(); + DashboardView.SetupDashboardCalendars(); // Zaul TODO: needed? return DashboardView.openDashboard(dashboardDoc); } @@ -545,7 +544,7 @@ export class CollectionDockingView extends CollectionSubView() { tabCreated = (tab: { contentItem: { element: HTMLElement[] } }) => { this.tabMap.add(tab); // InitTab is added to the tab's HTMLElement in TabDocView - const tabdocviewContent = tab.contentItem.element[0]?.firstChild?.firstChild as unknown as TabHTMLElement; + const tabdocviewContent = tab.contentItem.element[0]?.firstChild?.firstChild as TabHTMLElement; tabdocviewContent?.InitTab?.(tab); // have to explicitly initialize tabs that reuse contents from previous tabs (ie, when dragging a tab around a new tab is created for the old content) }; diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss index 3ec875df4..45d9394ed 100644 --- a/src/client/views/collections/CollectionMenu.scss +++ b/src/client/views/collections/CollectionMenu.scss @@ -6,7 +6,7 @@ align-content: center; justify-content: space-between; background-color: $dark-gray; - height: 35px; + height: 40px; border-bottom: $standard-border; padding: 0 10px; align-items: center; diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 5d32482c3..581201a20 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -296,7 +296,7 @@ export function CollectionSubView<X>() { return false; } - protected async onExternalDrop(e: React.DragEvent, options: DocumentOptions, completed?: (docs: Doc[]) => void) { + protected async onExternalDrop(e: React.DragEvent, options: DocumentOptions = {}, completed?: (docs: Doc[]) => void) { if (e.ctrlKey) { e.stopPropagation(); // bcz: this is a hack to stop propagation when dropping an image on a text document with shift+ctrl return; diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index c9e934448..7418d4360 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -1,4 +1,3 @@ -/* eslint-disable react/jsx-props-no-spreading */ import { IReactionDisposer, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; @@ -16,7 +15,6 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { FieldView } from '../nodes/FieldView'; import { OpenWhere } from '../nodes/OpenWhere'; -import { CollectionCalendarView } from './CollectionCalendarView'; import { CollectionCardView } from './CollectionCardDeckView'; import { CollectionCarousel3DView } from './CollectionCarousel3DView'; import { CollectionCarouselView } from './CollectionCarouselView'; diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index d2514dfd1..5444a7a57 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -467,7 +467,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> { return false; } - refTransform = (ref: HTMLElement | undefined | null) => { + refTransform = (ref: HTMLDivElement | undefined | null) => { if (!ref) return this.ScreenToLocalTransform(); const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(ref); return new Transform(-translateX, -translateY, 1).scale(1 / scale); diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx index c17371151..51add85a8 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormInfoState.tsx @@ -46,7 +46,6 @@ export function InfoState( gif?: string, entryFunc?: () => unknown ) { - // eslint-disable-next-line new-cap return new infoState(msg, arcs, gif, entryFunc); } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index f106eba26..d8678eebc 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -30,7 +30,7 @@ import { CompileScript } from '../../../util/Scripting'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { freeformScrollMode, SnappingManager } from '../../../util/SnappingManager'; import { Transform } from '../../../util/Transform'; -import { undoable, UndoManager } from '../../../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager'; import { Timeline } from '../../animationtimeline/Timeline'; import { ContextMenu } from '../../ContextMenu'; import { InkingStroke } from '../../InkingStroke'; @@ -42,6 +42,8 @@ import { FocusViewOptions } from '../../nodes/FocusViewOptions'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { OpenWhere, OpenWhereMod } from '../../nodes/OpenWhere'; import { PinDocView, PinProps } from '../../PinFuncs'; +import { AnnotationPalette } from '../../smartdraw/AnnotationPalette'; +import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler'; import { StyleProp } from '../../StyleProp'; import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView'; import { TreeViewType } from '../CollectionTreeViewType'; @@ -115,6 +117,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _panZoomTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan. set to 0 to use document's transition time or default of 0 @observable _firstRender = false; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown. could be used for performance improvement @observable _showAnimTimeline = false; + @observable _showDrawingEditor = false; @observable _deleteList: DocumentView[] = []; @observable _timelineRef = React.createRef<Timeline>(); @observable _marqueeViewRef = React.createRef<MarqueeView>(); @@ -135,7 +138,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine); } @computed get childPointerEvents() { - return SnappingManager.IsResizing + return falseSnappingManager.IsResizing ? 'none' : (this._props.childPointerEvents?.() ?? (this._props.viewDefDivClick || // @@ -492,28 +495,31 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection 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 + const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY)); switch (Doc.ActiveTool) { - 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.Highlighter: + case InkTool.Write: + 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.StrokeEraser: case InkTool.SegmentEraser: - this._batch = UndoManager.StartBatch('collectionErase'); - this._eraserPts.length = 0; - setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction); - break; case InkTool.RadiusEraser: this._batch = UndoManager.StartBatch('collectionErase'); this._eraserPts.length = 0; - setupMoveUpEvents(this, e, this.onRadiusEraserMove, this.onEraserUp, emptyFunction); + setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, this.onEraserClick, hit !== -1); + e.stopPropagation(); + break; + case InkTool.SmartDraw: + setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, () => this.showSmartDraw(e.pageX, e.pageY), hit !== -1); + e.stopPropagation(); break; case InkTool.None: if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) { - const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY)); - setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, hit !== -1, false); + const ahit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY)); + setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, ahit !== -1, false); } break; - default: + default: } } } @@ -536,24 +542,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection default: { const { points } = ge; 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( - points, - { title: ge.gesture.toString(), - x: B.x - inkWidth / 2, - y: B.y - inkWidth / 2, - _width: B.width + inkWidth, - _height: B.height + inkWidth, - stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore - inkWidth, - ActiveInkColor(), - ActiveInkBezierApprox(), - ActiveFillColor(), - ActiveArrowStart(), - ActiveArrowEnd(), - ActiveDash(), - ActiveIsInkMask() - ); + const inkDoc = this.createInkDoc(points, B); if (Doc.ActiveTool === InkTool.Write) { this.unprocessedDocs.push(inkDoc); CollectionFreeFormView.collectionsWithUnprocessedInk.add(this); @@ -605,96 +594,76 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection /** * 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. + * By default this iterates through all intersected ink strokes, determines which parts of a stroke need to be erased based on the type + * of eraser, draws back the ink segments to keep, and deletes the original stroke. + * + * Radius eraser: erases strokes by intersecting them with a circle of variable radius. Essentially creates an InkField for the + * eraser circle, then determines its intersections with other ink strokes. Each stroke's DocumentView and its + * intersection t-values are put into a map, which gets looped through to take out the erased parts. */ - @action - onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => { + erase = (e: PointerEvent, delta: number[]) => { + e.stopImmediatePropagation(); const currPoint = { X: e.clientX, Y: e.clientY }; this._eraserPts.push([currPoint.X, currPoint.Y]); this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5)); - // if (this._eraserLock) return false; // leaving this commented out in case the idea is revisited in the future - 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.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 (Doc.ActiveTool !== InkTool.StrokeEraser) { - // this._eraserLock++; - const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it - const newStrokes = segments?.map(segment => { - const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]); - const bounds = InkField.getBounds(points); - const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height); - const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale; - return Docs.Create.InkDocument( - points, - { title: 'stroke', - x: B.x - inkWidth / 2, - y: B.y - inkWidth / 2, - _width: B.width + inkWidth, - _height: B.height + inkWidth, - stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore - inkWidth, - ActiveInkColor(), - ActiveInkBezierApprox(), - ActiveFillColor(), - ActiveArrowStart(), - ActiveArrowEnd(), - ActiveDash(), - ActiveIsInkMask() - ); - }); - newStrokes && this.addDocument?.(newStrokes); - // setTimeout(() => this._eraserLock--); + if (Doc.ActiveTool === InkTool.RadiusEraser) { + const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint); + strokeMap.forEach((intersects, stroke) => { + if (!this._deleteList.includes(stroke)) { + this._deleteList.push(stroke); + SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1'); + SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black'); + const segments = this.radiusErase(stroke, intersects.sort()); + segments?.forEach(segment => + this.forceStrokeGesture( + e, + Gestures.Stroke, + segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.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. - intersect.inkView.layoutDoc.opacity = 0; - intersect.inkView.layoutDoc.dontIntersect = true; - } - }); + stroke.layoutDoc.opacity = 0; + stroke.layoutDoc.dontIntersect = true; + }); + } else { + 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.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 (Doc.ActiveTool !== InkTool.StrokeEraser) { + // this._eraserLock++; + const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it + const newStrokes = segments?.map(segment => { + const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]); + return this.createInkDoc(points); + }); + newStrokes && this.addDocument?.(newStrokes); + // setTimeout(() => this._eraserLock--); + } + } + }); + } return false; }; - /** - * Erases strokes by intersecting them with a circle of variable radius. Essentially creates an InkField for the - * eraser circle, then determines its intersections with other ink strokes. Each stroke's DocumentView and its - * intersection t-values are put into a map, which gets looped through to take out the erased parts. - * @param e - * @param down - * @param delta - * @returns - */ @action - onRadiusEraserMove = (e: PointerEvent, down: number[], delta: number[]) => { - const currPoint = { X: e.clientX, Y: e.clientY }; - this._eraserPts.push([currPoint.X, currPoint.Y]); - this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5)); - const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint); - - strokeMap.forEach((intersects, stroke) => { - if (!this._deleteList.includes(stroke)) { - this._deleteList.push(stroke); - SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1'); - SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black'); - const segments = this.radiusErase(stroke, intersects.sort()); - segments?.forEach(segment => - this.forceStrokeGesture( - e, - Gestures.Stroke, - segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]) - ) - ); - } - stroke.layoutDoc.opacity = 0; - stroke.layoutDoc.dontIntersect = true; - }); + onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => { + this.erase(e, delta); + // if (this._eraserLock) return false; // leaving this commented out in case the idea is revisited in the future return false; }; - forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData) => { - this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points))); + @action + onEraserClick = (e: PointerEvent) => { + e.preventDefault(); + e.stopImmediatePropagation(); + this.erase(e, [0, 0]); + }; + + forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: string) => { + this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points), text)); }; onPointerMove = (e: PointerEvent) => { @@ -722,7 +691,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // increase radius slightly based on the erased stroke's width, added to make eraser look more realistic const radius = ActiveEraserWidth() + 5 + inkStrokeWidth * 0.1; // add 5 to prevent eraser from being too small const c = 0.551915024494; // circle tangent length to side ratio - const movement = { x: endInkCoordsIn.X - startInkCoordsIn.X, y: endInkCoordsIn.Y - startInkCoordsIn.Y }; + const movement = { x: Math.max(endInkCoordsIn.X - startInkCoordsIn.X, 1), y: Math.max(endInkCoordsIn.Y - startInkCoordsIn.Y, 1) }; const moveLen = Math.sqrt(movement.x ** 2 + movement.y ** 2); const direction = { x: (movement.x / moveLen) * radius, y: (movement.y / moveLen) * radius }; const normal = { x: -direction.y, y: direction.x }; // prettier-ignore @@ -889,13 +858,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection strokeToTVals.set(inkView, [Math.floor(inkData.length / 4) + 1]); } } - for (let i = 0; i < inkData.length - 3; i += 4) { // iterate over each segment of bezier curve for (let j = 0; j < eraserInkData.length - 3; j += 4) { const intersectCurve: Bezier = InkField.Segment(inkData, i); // other curve const eraserCurve: Bezier = InkField.Segment(eraserInkData, j); // eraser curve - this.bintersects(intersectCurve, eraserCurve).forEach((val: string | number, k: number) => { + InkField.bintersects(intersectCurve, eraserCurve).forEach((val: string | number, k: number) => { // Converting the Bezier.js Split type to a t-value number. const t = +val.toString().split('/')[0]; if (k % 2 === 0) { @@ -1169,26 +1137,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return this.getClosestTs(tVals, excludeT, startIndex, mid - 1); }; - // for some reason bezier.js doesn't handle the case of intersecting a linear curve, so we wrap the intersection - // call in a test for linearity - bintersects = (curve: Bezier, otherCurve: Bezier) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((curve as any)._linear) { - // bezier.js doesn't intersect properly if the curve is actually a line -- so get intersect other curve against this line, then figure out the t coordinates of the intersection on this line - const intersections = otherCurve.lineIntersects({ p1: curve.points[0], p2: curve.points[3] }); - if (intersections.length) { - const intPt = otherCurve.get(intersections[0]); - const intT = curve.project(intPt).t; - return intT ? [intT] : []; - } - } - // eslint-disable-next-line @typescript-eslint/no-explicit-any - if ((otherCurve as any)._linear) { - return curve.lineIntersects({ p1: otherCurve.points[0], p2: otherCurve.points[3] }); - } - return curve.intersects(otherCurve); - }; - /** * Determines all possible intersections of the current curve of the intersected ink stroke with all other curves of all * ink strokes in the current collection. @@ -1220,7 +1168,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (apt.d !== undefined && apt.d < 1 && apt.t !== undefined && !tVals.includes(apt.t)) { tVals.push(apt.t); } - this.bintersects(curve, otherCurve).forEach((val: string | number, ival: number) => { + InkField.bintersects(curve, otherCurve).forEach((val: string | number, ival: number) => { // Converting the Bezier.js Split type to a t-value number. const t = +val.toString().split('/')[0]; if (ival % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack! don't know why but intersection points are doubled from bezier.js (but not identical). @@ -1233,6 +1181,108 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return tVals; }; + /** + * Creates an ink document to add to the freeform canvas. + */ + createInkDoc = (points: InkData, transformedBounds?: { x: number; y: number; width: number; height: number }) => { + const bounds = InkField.getBounds(points); + const B = transformedBounds || this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height); + const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale; + return Docs.Create.InkDocument( + points, + { title: 'stroke', + x: B.x - inkWidth / 2, + y: B.y - inkWidth / 2, + _width: B.width + inkWidth, + _height: B.height + inkWidth, + stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore + inkWidth, + ActiveInkColor(), + ActiveInkBezierApprox(), + ActiveFillColor(), + ActiveArrowStart(), + ActiveArrowEnd(), + ActiveDash(), + ActiveIsInkMask() + ); + }; + + @action + showSmartDraw = (x: number, y: number) => { + SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc; + SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing; + SmartDrawHandler.Instance.AddDrawing = this.addDrawing; + SmartDrawHandler.Instance.displaySmartDrawHandler(x, y); + }; + + _drawing: Doc[] = []; + _drawingContainer: Doc | undefined = undefined; + /** + * Function that creates a drawing--a group of ink strokes--to go with the smart draw function. + */ + @undoBatch + createDrawingDoc = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => { + this._drawing = []; + const xf = this.screenToFreeformContentsXf; + strokeData.forEach((stroke: [InkData, string, string]) => { + const bounds = InkField.getBounds(stroke[0]); + const B = xf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height); + const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale; + const inkDoc = Docs.Create.InkDocument( + stroke[0], + { title: 'stroke', + x: B.x - inkWidth / 2, + y: B.y - inkWidth / 2, + _width: B.width + inkWidth, + _height: B.height + inkWidth, + stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore + inkWidth, + opts.autoColor ? stroke[1] : ActiveInkColor(), + ActiveInkBezierApprox(), + stroke[2] === 'none' ? ActiveFillColor() : stroke[2], + ActiveArrowStart(), + ActiveArrowEnd(), + ActiveDash(), + ActiveIsInkMask() + ); + this._drawing.push(inkDoc); + }); + return MarqueeView.getCollection(this._drawing, undefined, true, { left: opts.x, top: opts.y, width: 1, height: 1 }); + }; + + /** + * Part of regenerating a drawing--deletes the old drawing. + */ + removeDrawing = (useLastContainer: boolean, doc?: Doc) => { + this._batch = UndoManager.StartBatch('regenerateDrawing'); + if (useLastContainer && this._drawingContainer) { + this._props.removeDocument?.(this._drawingContainer); + } else if (doc) { + const docData = doc[DocData]; + const children = DocListCast(docData.data); + this._props.removeDocument?.(doc); + this._props.removeDocument?.(children); + } + this._drawing = []; + }; + + /** + * Adds the created drawing to the freeform canvas and sets the metadata. + */ + addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string) => { + const docData = doc[DocData]; + docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text; + docData.width = opts.size; + docData.drawingInput = opts.text; + docData.drawingComplexity = opts.complexity; + docData.drawingColored = opts.autoColor; + docData.drawingSize = opts.size; + docData.drawingData = gptRes; + this._drawingContainer = doc; + this.addDocument(doc); + this._batch?.end(); + }; + @action zoom = (pointX: number, pointY: number, deltaY: number): void => { if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return; @@ -1803,33 +1853,34 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection Object.values(this._disposers).forEach(disposer => disposer?.()); } - updateIcon = () => { + updateIcon = (usePanelDimensions?: boolean) => { const contentDiv = this.DocumentView?.().ContentDiv; - contentDiv && - UpdateIcon( - this.layoutDoc[Id] + '_icon_' + new Date().getTime(), - 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; - } - ); + return !contentDiv + ? new Promise<void>(res => res()) + : UpdateIcon( + this.layoutDoc[Id] + '_icon_' + new Date().getTime(), + contentDiv, + usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), + usePanelDimensions ? this._props.PanelHeight() : 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; + } + ); }; @action onCursorMove = (e: React.PointerEvent) => { - this._eraserX = e.clientX; - this._eraserY = e.clientY; - // super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); + const locPt = this.ScreenToLocalBoxXf().transformPoint(e.clientX, e.clientY); + this._eraserX = locPt[0]; + this._eraserY = locPt[1]; }; @action @@ -1910,7 +1961,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection icon: 'eye', }); 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' }); + !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.Document.isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' }); @@ -1930,6 +1981,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }), icon: 'eye', }); + optionItems.push({ + description: 'Show Drawing Editor', + event: action(() => { + SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc; + SmartDrawHandler.Instance.AddDrawing = this.addDrawing; + SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing; + !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate(); + }), + icon: 'pen-to-square', + }); + optionItems.push({ + description: this.Document.savedAsAnno ? 'Saved as Annotation!' : 'Save to Annotation Palette', + event: action(undoable(async () => await AnnotationPalette.addToPalette(this.Document), 'save to palette')), + icon: this.Document.savedAsAnno ? 'clipboard-check' : 'file-arrow-down', + }); this._props.renderDepth && optionItems.push({ description: 'Use Background Color as Default', @@ -1945,6 +2011,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection !options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); const mores = ContextMenu.Instance.findByDescription('More...'); const moreItems = mores?.subitems ?? []; + moreItems.push({ + description: 'recognize all ink', + event: () => { + this.unprocessedDocs.push(...this.childDocs.filter(doc => doc.type === DocumentType.INK)); + CollectionFreeFormView.collectionsWithUnprocessedInk.add(this); + }, + icon: 'pen', + }); !mores && ContextMenu.Instance.addItem({ description: 'More...', subitems: moreItems, icon: 'eye' }); }; @@ -2133,8 +2207,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onPointerMove={this.onCursorMove} style={{ position: 'fixed', - left: this._eraserX - 60, - top: this._eraserY - 100, + left: this._eraserX, + top: this._eraserY, width: (ActiveEraserWidth() + 5) * 2, height: (ActiveEraserWidth() + 5) * 2, borderRadius: '50%', diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss index e7413bf8e..9b8727e1a 100644 --- a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss +++ b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss @@ -42,3 +42,17 @@ } } } + +.complexity-slider { + width: 50%; /* Full-width */ + height: 25px; /* Specified height */ + background: #d3d3d3; /* Grey background */ + outline: none; /* Remove outline */ + opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */ + -webkit-transition: 0.2s; /* 0.2 seconds transition on hover */ + transition: opacity 0.2s; + + :hover { + opacity: 1; /* Fully shown on mouse-over */ + } +} diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index 44c916ab9..de65b240f 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -3,6 +3,7 @@ import { IconButton } from 'browndash-components'; import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { Doc } from '../../../../fields/Doc'; import { unimplementedFunction } from '../../../../Utils'; import { SettingsManager } from '../../../util/SettingsManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; @@ -12,7 +13,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { // eslint-disable-next-line no-use-before-define static Instance: MarqueeOptionsMenu; - public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean) => void = unimplementedFunction; + public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean, selection?: Doc[]) => Doc | void = unimplementedFunction; public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public showMarquee: () => void = unimplementedFunction; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index ccb6bc9be..10709cc00 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -2,11 +2,11 @@ import { action, computed, makeObservable, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, lightOrDark, returnFalse } from '../../../../ClientUtils'; -import { intersectRect } from '../../../../Utils'; +import { intersectRect, unimplementedFunction } from '../../../../Utils'; import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; -import { InkTool } from '../../../../fields/InkField'; +import { InkData, InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { Cast, NumCast, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; @@ -57,6 +57,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps return { left: NumCast(pinDoc._freeform_panX) - panelWidth / 2 / ps, top: NumCast(pinDoc._freeform_panY) - panelHeight / 2 / ps, width: panelWidth / ps, height: panelHeight / ps }; } + // eslint-disable-next-line no-use-before-define static Instance: MarqueeView; constructor(props: SubCollectionViewProps & MarqueeViewProps) { @@ -88,6 +89,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps return bounds; } + public AddInkDoc: (points: InkData) => Doc | void = unimplementedFunction; + componentDidMount() { this._props.setPreviewCursor?.(this.setPreviewCursor); } @@ -363,7 +366,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps this.hideMarquee(); }); - getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, makeGroup: Opt<boolean>) => { + public static getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, makeGroup: Opt<boolean>, bounds: MarqueeViewBounds) => { const newCollection = creator ? creator(selected, { title: 'nested stack' }) : ((doc: Doc) => { @@ -375,14 +378,13 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); newCollection.isSystem = undefined; - newCollection._width = this.Bounds.width; - newCollection._height = this.Bounds.height; + newCollection._width = bounds.width || 1; // if width/height are unset/0, then groups won't autoexpand to contain their children + newCollection._height = bounds.height || 1; newCollection._dragWhenActive = makeGroup; - newCollection.x = this.Bounds.left; - newCollection.y = this.Bounds.top; + newCollection.x = bounds.left; + newCollection.y = bounds.top; newCollection.layout_fitWidth = true; selected.forEach(d => Doc.SetContainer(d, newCollection)); - this.hideMarquee(); return newCollection; }); @@ -419,7 +421,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps this._props.removeDocument?.(selected); } - const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group); + const newCollection = MarqueeView.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group, this.Bounds); newCollection._freeform_panX = this.Bounds.left + this.Bounds.width / 2; newCollection._freeform_panY = this.Bounds.top + this.Bounds.height / 2; newCollection._currentFrame = activeFrame; @@ -427,6 +429,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps this._props.selectDocuments([newCollection]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); + return newCollection; }); /** @@ -455,20 +458,19 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps let x_offset = 0; let y_offset = 0; let row_count = 0; + const newColDim = 900; for (const label of labelGroups) { - const newCollection = this.getCollection([], undefined, false); - newCollection._width = 900; - newCollection._height = 900; - newCollection._x = this.Bounds.left; - newCollection._y = this.Bounds.top; + const newCollection = MarqueeView.getCollection([], undefined, false, this.Bounds); + newCollection._x = this.Bounds.left + x_offset; + newCollection._y = this.Bounds.top + y_offset; + newCollection._width = newColDim; + newCollection._height = newColDim; newCollection._freeform_panX = this.Bounds.left + this.Bounds.width / 2; newCollection._freeform_panY = this.Bounds.top + this.Bounds.height / 2; - newCollection._x = (newCollection._x as number) + x_offset; - newCollection._y = (newCollection._y as number) + y_offset; - x_offset += (newCollection._width as number) + 40; + x_offset += newColDim + 40; row_count += 1; if (row_count == 3) { - y_offset += (newCollection._height as number) + 40; + y_offset += newColDim + 40; x_offset = 0; row_count = 0; } @@ -547,7 +549,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps }; touchesLine(r1: { left: number; top: number; width: number; height: number }) { - // eslint-disable-next-line no-restricted-syntax for (const lassoPt of this._lassoPts) { const topLeft = this.Transform.transformPoint(lassoPt[0], lassoPt[1]); if (r1.left < topLeft[0] && topLeft[0] < r1.left + r1.width && r1.top < topLeft[1] && topLeft[1] < r1.top + r1.height) { @@ -568,7 +569,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps let hasLeft = false; let hasBottom = false; let hasRight = false; - // eslint-disable-next-line no-restricted-syntax for (const lassoPt of this._lassoPts) { const truePoint = this.Transform.transformPoint(lassoPt[0], lassoPt[1]); hasLeft = hasLeft || (truePoint[0] > tl[0] && truePoint[0] < r1.left && truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height); @@ -685,8 +685,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps <div className="marqueeView" ref={r => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - r?.addEventListener('dashDragMovePause', this.onDragMovePause as any); + r?.addEventListener('dashDragMovePause', this.onDragMovePause as EventListenerOrEventListenerObject); this.MarqueeRef = r; }} style={{ diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index 8b0639b3b..325628d53 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -32,7 +32,6 @@ import './CollectionSchemaView.scss'; import { SchemaColumnHeader } from './SchemaColumnHeader'; import { SchemaRowBox } from './SchemaRowBox'; -// eslint-disable-next-line @typescript-eslint/no-var-requires const { SCHEMA_NEW_NODE_HEIGHT } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore export const FInfotoColType: { [key: string]: ColumnType } = { |
