diff options
Diffstat (limited to 'src/client/views/collections')
3 files changed, 72 insertions, 332 deletions
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index b8257ff31..467191735 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -7,7 +7,6 @@ import { action, computed, IReactionDisposer, makeObservable, observable, reacti import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import * as React from 'react'; -import { TbAlpha } from 'react-icons/tb'; import { ClientUtils, DashColor, lightOrDark, OmitKeys, returnFalse, returnZero, setupMoveUpEvents, UpdateIcon } from '../../../../ClientUtils'; import { DateField } from '../../../../fields/DateField'; import { ActiveEraserWidth, ActiveInkWidth, Doc, DocListCast, Field, FieldType, Opt, SetActiveInkColor, SetActiveInkWidth } from '../../../../fields/Doc'; @@ -56,7 +55,7 @@ import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannable import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors'; import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; -import { SmartDrawHandler } from './SmartDrawHandler'; +import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler'; @observer class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> { @@ -120,6 +119,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>(); @@ -514,7 +514,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection e.stopPropagation(); break; case InkTool.SmartDraw: - setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, this.createDrawing, hit !== -1); + setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, this.showSmartDraw, hit !== -1); e.stopPropagation(); case InkTool.None: if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) { @@ -566,6 +566,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } }; + @action onEraserUp = (): void => { this._deleteList.lastElement()?._props.removeDocument?.(this._deleteList.map(ink => ink.Document)); @@ -607,12 +608,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection _eraserPts: number[][] = []; // keep track of the last few eraserPts to make the eraser circle 'stretch' 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 (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); @@ -682,9 +683,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action onEraserClick = (e: PointerEvent, doubleTap?: boolean) => { + e.preventDefault(); + e.stopImmediatePropagation(); this.erase(e, [0, 0]); - e.stopPropagation(); - return false; }; /** @@ -696,32 +697,32 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection * @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; - }); - return false; - }; + // @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; + // }); + // return false; + // }; forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: any) => { this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points), text)); @@ -1263,15 +1264,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @action - createDrawing = (e: PointerEvent, doubleTap?: boolean) => { - SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createInkStrokes); + showSmartDraw = (e: PointerEvent, doubleTap?: boolean) => { + SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createDrawing, this.removeDrawing); }; + _drawing: Doc[] = []; @undoBatch - createInkStrokes = (strokeData: [InkData, string, string][]) => { + createDrawing = (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => { strokeData.forEach((stroke: [InkData, string, string]) => { - // const points: InkData = FitCurve(inkData, 20) as InkData; - // const allPts = GenerateControlPoints(inkData, alpha); const bounds = InkField.getBounds(stroke[0]); const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height); const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale; @@ -1288,8 +1288,33 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection undefined, stroke[2] === 'none' ? undefined : stroke[2] ); + this._drawing.push(inkDoc); this.addDocument(inkDoc); }); + // const collection = this._marqueeViewRef.current?.collection(undefined, false, this._drawing); + // if (collection) { + // const docData = collection[DocData]; + // docData.title = opts.text; + // docData.drawingInput = opts.text; + // docData.drawingComplexity = opts.complexity; + // docData.drawingColored = opts.autoColor; + // docData.drawingSize = opts.size; + // docData.drawingData = gptRes; + // } + this._batch?.end(); + }; + + removeDrawing = (doc?: Doc) => { + this._batch = UndoManager.StartBatch('regenerateDrawing'); + if (doc) { + const docData: Doc = doc[DocData]; + const children = docData.data as unknown as Doc[]; + this._props.removeDocument?.(doc); + this._props.removeDocument?.(children); + } else { + this._props.removeDocument?.(this._drawing); + } + this._drawing = []; }; @action @@ -1995,6 +2020,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }), icon: 'eye', }); + optionItems.push({ + description: (this._showDrawingEditor ? 'Close' : 'Show') + ' Drawing Editor', + event: action(() => { + this._showDrawingEditor = !this._showDrawingEditor; + this._showDrawingEditor ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10, this.createDrawing, this.removeDrawing) : SmartDrawHandler.Instance.hideRegenerate(); + }), + icon: 'pen-to-square', + }); this._props.renderDepth && optionItems.push({ description: 'Use Background Color as Default', diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index dc15c83c5..23cf487ec 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -36,6 +36,7 @@ import { CollectionFreeFormView } from './CollectionFreeFormView'; import { ImageLabelHandler } from './ImageLabelHandler'; import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; import './MarqueeView.scss'; +import { collectionOf } from '@turf/turf'; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -426,6 +427,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps this._props.selectDocuments([newCollection]); MarqueeOptionsMenu.Instance.fadeOut(true); this.hideMarquee(); + return newCollection; }); /** diff --git a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx b/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx deleted file mode 100644 index edb814172..000000000 --- a/src/client/views/collections/collectionFreeForm/SmartDrawHandler.tsx +++ /dev/null @@ -1,295 +0,0 @@ -import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, makeObservable, observable } from 'mobx'; -import { observer } from 'mobx-react'; -import React from 'react'; -import { SettingsManager } from '../../../util/SettingsManager'; -import { ObservableReactComponent } from '../../ObservableReactComponent'; -import { Button, IconButton, Size } from 'browndash-components'; -import ReactLoading from 'react-loading'; -import { AiOutlineSend } from 'react-icons/ai'; -import './ImageLabelHandler.scss'; -import { gptAPICall, GPTCallType } from '../../../apis/gpt/GPT'; -import { InkData } from '../../../../fields/InkField'; -import { SVGToBezier } from '../../../util/bezierFit'; -const { parse } = require('svgson'); -import { Slider, Switch } from '@mui/material'; -import { IconProp } from '@fortawesome/fontawesome-svg-core'; -import { Flex } from '@adobe/react-spectrum'; -import { Row } from 'react-aria-components'; -import { UndoManager } from '../../../util/UndoManager'; -import e from 'cors'; - -@observer -export class SmartDrawHandler extends ObservableReactComponent<{}> { - static Instance: SmartDrawHandler; - - @observable private _display: boolean = false; - @observable private _pageX: number = 0; - @observable private _pageY: number = 0; - @observable private _yRelativeToTop: boolean = true; - @observable private _isLoading: boolean = false; - @observable private _userInput: string = ''; - @observable private _showOptions: boolean = false; - @observable private _menuIcon: string = 'caret-right'; - @observable private _complexity: number = 5; - @observable private _size: number = 300; - @observable private _autoColor: boolean = true; - @observable private _showRegenerate: boolean = false; - private _addToDocFunc: (strokeList: [InkData, string, string][]) => void = () => {}; - private _lastX: number = 0; - private _lastY: number = 0; - - constructor(props: any) { - super(props); - makeObservable(this); - SmartDrawHandler.Instance = this; - } - - @action - setUserInput = (input: string) => { - this._userInput = input; - }; - - @action - displaySmartDrawHandler = (x: number, y: number, addToDoc: (strokeData: [InkData, string, string][]) => void) => { - this._pageX = x; - this._pageY = y; - this._display = true; - this._addToDocFunc = addToDoc; - }; - - hideSmartDrawHandler = () => { - this._showRegenerate = false; - this._display = false; - this._isLoading = false; - this._showOptions = false; - this._menuIcon = 'caret-right'; - }; - - hideRegenerate = () => { - this._showRegenerate = false; - this._userInput = ''; - this._complexity = 5; - this._size = 300; - this._autoColor = true; - this._isLoading = false; - }; - - toggleMenu = () => { - this._showOptions = !this._showOptions; - this._menuIcon === 'caret-right' ? (this._menuIcon = 'caret-down') : (this._menuIcon = 'caret-right'); - }; - - @action - drawWithGPT = async (e: React.MouseEvent<Element, MouseEvent>, startPoint: { X: number; Y: number }, input: string, regenerate: boolean = false) => { - if (this._userInput === '') return; - e.stopPropagation(); - this._lastX = startPoint.X; - this._lastY = startPoint.Y; - this._isLoading = true; - this._showOptions = false; - try { - const res = await gptAPICall(`"${input}", "${this._complexity}", "${this._size}"`, GPTCallType.DRAW); - if (!res) { - console.error('GPT call failed'); - return; - } - const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g); - if (svg) { - const svgObject = await parse(svg[0]); - const svgStrokes: any = svgObject.children; - const strokeData: [InkData, string, string][] = []; - svgStrokes.forEach((child: any) => { - const convertedBezier: InkData = SVGToBezier(child.name, child.attributes); - strokeData.push([ - convertedBezier.map(point => { - return { X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 }; - }), - this._autoColor ? child.attributes.stroke : undefined, - this._autoColor ? child.attributes.fill : undefined, - ]); - }); - if (regenerate) UndoManager.Undo(); - this._addToDocFunc(strokeData); - } - } catch (err) { - console.error('GPT call failed', err); - } - this.hideSmartDrawHandler(); - this._showRegenerate = true; - }; - - regenerate = (e: React.MouseEvent<Element, MouseEvent>) => { - this.drawWithGPT(e, { X: this._lastX, Y: this._lastY }, `Regenerate the item "${this._userInput}"`, true); - }; - - render() { - if (this._display) { - return ( - <div - id="label-handler" - className="contextMenu-cont" - style={{ - display: this._display ? '' : 'none', - left: this._pageX, - ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }), - background: SettingsManager.userBackgroundColor, - color: SettingsManager.userColor, - }}> - <div> - <IconButton - tooltip={'Cancel'} - onClick={() => { - this.hideSmartDrawHandler(); - this.hideRegenerate(); - }} - icon={<FontAwesomeIcon icon="xmark" />} - color={SettingsManager.userColor} - style={{ width: '19px' }} - /> - <input - aria-label="label-input" - id="new-label" - type="text" - style={{ color: 'black' }} - value={this._userInput} - onChange={e => { - this.setUserInput(e.target.value); - }} - placeholder="Enter item to draw" - /> - <IconButton - tooltip="Advanced Options" - icon={<FontAwesomeIcon icon={this._showOptions ? 'caret-down' : 'caret-right'} />} - color={SettingsManager.userColor} - style={{ width: '14px' }} - onClick={() => { - this._showOptions = !this._showOptions; - }} - /> - <Button - style={{ alignSelf: 'flex-end' }} - text="Send" - icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} - iconPlacement="right" - color={SettingsManager.userColor} - onClick={e => { - this.drawWithGPT(e, { X: e.clientX, Y: e.clientY }, this._userInput); - }} - /> - </div> - {this._showOptions && ( - <> - <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around' }}> - <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '30%' }}> - Auto color - <Switch - sx={{ - '& .MuiSwitch-switchBase.Mui-checked': { - color: SettingsManager.userColor, - }, - '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { - backgroundColor: SettingsManager.userVariantColor, - }, - }} - defaultChecked={true} - size="small" - onChange={() => (this._autoColor = !this._autoColor)} - /> - </div> - <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '31%' }}> - Complexity - <Slider - sx={{ - '& .MuiSlider-thumb': { - color: SettingsManager.userColor, - '&.Mui-focusVisible, &:hover, &.Mui-active': { - boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`, - }, - }, - '& .MuiSlider-track': { - color: SettingsManager.userVariantColor, - }, - '& .MuiSlider-rail': { - color: SettingsManager.userColor, - }, - }} - style={{ width: '80%' }} - min={1} - max={10} - step={1} - size="small" - value={this._complexity} - onChange={(e, val) => { - this._complexity = val as number; - }} - valueLabelDisplay="auto" - /> - </div> - <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '39%' }}> - Size (in pixels) - <Slider - sx={{ - '& .MuiSlider-thumb': { - color: SettingsManager.userColor, - '&.Mui-focusVisible, &:hover, &.Mui-active': { - boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`, - }, - }, - '& .MuiSlider-track': { - color: SettingsManager.userVariantColor, - }, - '& .MuiSlider-rail': { - color: SettingsManager.userColor, - }, - }} - style={{ width: '80%' }} - min={50} - max={700} - step={10} - size="small" - value={this._size} - onChange={(e, val) => { - this._size = val as number; - }} - valueLabelDisplay="auto" - /> - </div> - </div> - </> - )} - </div> - ); - } else if (this._showRegenerate) { - return ( - <div - id="smartdraw-options-menu" - className="contextMenu-cont" - style={{ - left: this._pageX, - ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }), - background: SettingsManager.userBackgroundColor, - color: SettingsManager.userColor, - }}> - <div - style={{ - display: 'flex', - flexDirection: 'row', - }}> - <IconButton tooltip="Cancel" onClick={this.hideRegenerate} icon={<FontAwesomeIcon icon="xmark" />} color={SettingsManager.userColor} style={{ width: '19px' }} /> - <IconButton - tooltip="Regenerate" - icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />} - color={SettingsManager.userColor} - onClick={e => { - this.regenerate(e); - }} - /> - </div> - </div> - ); - } else { - return <></>; - } - } -} |
