diff options
Diffstat (limited to 'src/client/views')
-rw-r--r-- | src/client/views/GestureOverlay.tsx | 150 | ||||
-rw-r--r-- | src/client/views/InkStrokeProperties.ts | 43 | ||||
-rw-r--r-- | src/client/views/InkTranscription.tsx | 4 | ||||
-rw-r--r-- | src/client/views/InkingStroke.tsx | 4 | ||||
-rw-r--r-- | src/client/views/PropertiesButtons.tsx | 4 | ||||
-rw-r--r-- | src/client/views/PropertiesView.scss | 6 | ||||
-rw-r--r-- | src/client/views/PropertiesView.tsx | 180 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 106 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx | 2 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/MarqueeView.tsx | 80 | ||||
-rw-r--r-- | src/client/views/nodes/formattedText/FormattedTextBox.tsx | 8 | ||||
-rw-r--r-- | src/client/views/pdf/AnchorMenu.tsx | 38 | ||||
-rw-r--r-- | src/client/views/pdf/PDFViewer.tsx | 10 | ||||
-rw-r--r-- | src/client/views/smartdraw/AnnotationPalette.tsx | 44 | ||||
-rw-r--r-- | src/client/views/smartdraw/SmartDrawHandler.tsx | 155 |
15 files changed, 609 insertions, 225 deletions
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 649208989..7eac583dd 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -141,7 +141,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil // if any of the shape is activated in the CollectionFreeFormViewChrome if (this.InkShape) { - this.makeBezierPolygon(this.InkShape, false); + GestureOverlay.makeBezierPolygon(this._points, this.InkShape, false); this.dispatchGesture(this.InkShape); this.primCreated(); } @@ -158,7 +158,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil case Gestures.Triangle: case Gestures.Rectangle: case Gestures.Circle: - this.makeBezierPolygon(result.Name, true); + GestureOverlay.makeBezierPolygon(this._points, result.Name, true); actionPerformed = this.dispatchGesture(result.Name); console.log(result.Name); console.log(); @@ -202,17 +202,17 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil this._points.length = 0; }; - makeBezierPolygon = (shape: string, gesture: boolean) => { - const xs = this._points.map(p => p.X); - const ys = this._points.map(p => p.Y); + public static makeBezierPolygon = (points: { X: number; Y: number }[], shape: string, gesture: boolean) => { + const xs = points.map(p => p.X); + const ys = points.map(p => p.Y); let right = Math.max(...xs); let left = Math.min(...xs); let bottom = Math.max(...ys); let top = Math.min(...ys); - const firstx = this._points[0].X; - const firsty = this._points[0].Y; - let lastx = this._points[this._points.length - 2].X; - let lasty = this._points[this._points.length - 2].Y; + const firstx = points[0].X; + const firsty = points[0].Y; + let lastx = points[points.length - 2].X; + let lasty = points[points.length - 2].Y; let fourth = (lastx - firstx) / 4; if (isNaN(fourth) || fourth === 0) { fourth = 0.01; @@ -223,15 +223,15 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil } // const b = firsty - m * firstx; if (shape === 'noRec') { - return false; + return undefined; } if (!gesture) { // if shape options is activated in inkOptionMenu // take second to last point because _point[length-1] is _points[0] - right = this._points[this._points.length - 2].X; - left = this._points[0].X; - bottom = this._points[this._points.length - 2].Y; - top = this._points[0].Y; + right = points[points.length - 2].X; + left = points[0].X; + bottom = points[points.length - 2].Y; + top = points[0].Y; if (shape !== Gestures.Arrow && shape !== Gestures.Line && shape !== Gestures.Circle) { if (left > right) { const temp = right; @@ -245,47 +245,47 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil } } } - this._points.length = 0; + points.length = 0; switch (shape) { case Gestures.Rectangle: - this._points.push({ X: left, Y: top }); // curr pt - this._points.push({ X: left, Y: top }); // curr first ctrl pt - this._points.push({ X: right, Y: top }); // next ctrl pt - this._points.push({ X: right, Y: top }); // next pt - - this._points.push({ X: right, Y: top }); // next pt - this._points.push({ X: right, Y: top }); // next first ctrl pt - this._points.push({ X: right, Y: bottom }); // next next ctrl pt - this._points.push({ X: right, Y: bottom }); // next next pt - - this._points.push({ X: right, Y: bottom }); - this._points.push({ X: right, Y: bottom }); - this._points.push({ X: left, Y: bottom }); - this._points.push({ X: left, Y: bottom }); - - this._points.push({ X: left, Y: bottom }); - this._points.push({ X: left, Y: bottom }); - this._points.push({ X: left, Y: top }); - this._points.push({ X: left, Y: top }); + points.push({ X: left, Y: top }); // curr pt + points.push({ X: left, Y: top }); // curr first ctrl pt + points.push({ X: right, Y: top }); // next ctrl pt + points.push({ X: right, Y: top }); // next pt + + points.push({ X: right, Y: top }); // next pt + points.push({ X: right, Y: top }); // next first ctrl pt + points.push({ X: right, Y: bottom }); // next next ctrl pt + points.push({ X: right, Y: bottom }); // next next pt + + points.push({ X: right, Y: bottom }); + points.push({ X: right, Y: bottom }); + points.push({ X: left, Y: bottom }); + points.push({ X: left, Y: bottom }); + + points.push({ X: left, Y: bottom }); + points.push({ X: left, Y: bottom }); + points.push({ X: left, Y: top }); + points.push({ X: left, Y: top }); break; case Gestures.Triangle: - this._points.push({ X: left, Y: bottom }); - this._points.push({ X: left, Y: bottom }); + points.push({ X: left, Y: bottom }); + points.push({ X: left, Y: bottom }); - this._points.push({ X: right, Y: bottom }); - this._points.push({ X: right, Y: bottom }); - this._points.push({ X: right, Y: bottom }); - this._points.push({ X: right, Y: bottom }); + points.push({ X: right, Y: bottom }); + points.push({ X: right, Y: bottom }); + points.push({ X: right, Y: bottom }); + points.push({ X: right, Y: bottom }); - this._points.push({ X: (right + left) / 2, Y: top }); - this._points.push({ X: (right + left) / 2, Y: top }); - this._points.push({ X: (right + left) / 2, Y: top }); - this._points.push({ X: (right + left) / 2, Y: top }); + points.push({ X: (right + left) / 2, Y: top }); + points.push({ X: (right + left) / 2, Y: top }); + points.push({ X: (right + left) / 2, Y: top }); + points.push({ X: (right + left) / 2, Y: top }); - this._points.push({ X: left, Y: bottom }); - this._points.push({ X: left, Y: bottom }); + points.push({ X: left, Y: bottom }); + points.push({ X: left, Y: bottom }); break; case Gestures.Circle: @@ -299,25 +299,25 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom)); // Dividing the circle into four equal sections, and fitting each section to a cubic Bézier curve. - this._points.push({ X: centerX, Y: centerY + radius }); // curr pt - this._points.push({ X: centerX + c * radius, Y: centerY + radius }); // curr first ctrl pt - this._points.push({ X: centerX + radius, Y: centerY + c * radius }); // next pt ctrl pt - this._points.push({ X: centerX + radius, Y: centerY }); // next pt - - this._points.push({ X: centerX + radius, Y: centerY }); // next pt - this._points.push({ X: centerX + radius, Y: centerY - c * radius }); // next first ctrl pt - this._points.push({ X: centerX + c * radius, Y: centerY - radius }); - this._points.push({ X: centerX, Y: centerY - radius }); - - this._points.push({ X: centerX, Y: centerY - radius }); - this._points.push({ X: centerX - c * radius, Y: centerY - radius }); - this._points.push({ X: centerX - radius, Y: centerY - c * radius }); - this._points.push({ X: centerX - radius, Y: centerY }); - - this._points.push({ X: centerX - radius, Y: centerY }); - this._points.push({ X: centerX - radius, Y: centerY + c * radius }); - this._points.push({ X: centerX - c * radius, Y: centerY + radius }); - this._points.push({ X: centerX, Y: centerY + radius }); + points.push({ X: centerX, Y: centerY + radius }); // curr pt + points.push({ X: centerX + c * radius, Y: centerY + radius }); // curr first ctrl pt + points.push({ X: centerX + radius, Y: centerY + c * radius }); // next pt ctrl pt + points.push({ X: centerX + radius, Y: centerY }); // next pt + + points.push({ X: centerX + radius, Y: centerY }); // next pt + points.push({ X: centerX + radius, Y: centerY - c * radius }); // next first ctrl pt + points.push({ X: centerX + c * radius, Y: centerY - radius }); + points.push({ X: centerX, Y: centerY - radius }); + + points.push({ X: centerX, Y: centerY - radius }); + points.push({ X: centerX - c * radius, Y: centerY - radius }); + points.push({ X: centerX - radius, Y: centerY - c * radius }); + points.push({ X: centerX - radius, Y: centerY }); + + points.push({ X: centerX - radius, Y: centerY }); + points.push({ X: centerX - radius, Y: centerY + c * radius }); + points.push({ X: centerX - c * radius, Y: centerY + radius }); + points.push({ X: centerX, Y: centerY + radius }); } break; @@ -328,11 +328,11 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil if (Math.abs(firsty - lasty) < 10 && Math.abs(firstx - lastx) > 10) { lasty = firsty; } - this._points.push({ X: firstx, Y: firsty }); - this._points.push({ X: firstx, Y: firsty }); + points.push({ X: firstx, Y: firsty }); + points.push({ X: firstx, Y: firsty }); - this._points.push({ X: lastx, Y: lasty }); - this._points.push({ X: lastx, Y: lasty }); + points.push({ X: lastx, Y: lasty }); + points.push({ X: lastx, Y: lasty }); break; case Gestures.Arrow: { @@ -347,16 +347,16 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle)); const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle)); const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle)); - this._points.push({ X: x1, Y: y1 }); - this._points.push({ X: x2, Y: y2 }); - this._points.push({ X: x3, Y: y3 }); - this._points.push({ X: x4, Y: y4 }); - this._points.push({ X: x2, Y: y2 }); + points.push({ X: x1, Y: y1 }); + points.push({ X: x2, Y: y2 }); + points.push({ X: x3, Y: y3 }); + points.push({ X: x4, Y: y4 }); + points.push({ X: x2, Y: y2 }); } break; default: } - return false; + return points; }; dispatchGesture = (gesture: Gestures, stroke?: InkData, text?: any) => { diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 3920ecc2a..35d628a4e 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -5,8 +5,8 @@ import { Doc, NumListCast, Opt } from '../../fields/Doc'; import { InkData, InkField, InkTool } from '../../fields/InkField'; import { List } from '../../fields/List'; import { listSpec } from '../../fields/Schema'; -import { Cast, NumCast } from '../../fields/Types'; -import { PointData } from '../../pen-gestures/GestureTypes'; +import { Cast, NumCast, toList } from '../../fields/Types'; +import { Gestures, PointData } from '../../pen-gestures/GestureTypes'; import { Point } from '../../pen-gestures/ndollar'; import { DocumentType } from '../documents/DocumentTypes'; import { undoBatch } from '../util/UndoManager'; @@ -14,6 +14,9 @@ import { FitOneCurve } from '../util/bezierFit'; import { InkingStroke } from './InkingStroke'; import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { DocumentView } from './nodes/DocumentView'; +import simplify from 'simplify-js'; +import { GestureUtils } from '../../pen-gestures/GestureUtils'; +import { GestureOverlay } from './GestureOverlay'; export class InkStrokeProperties { // eslint-disable-next-line no-use-before-define @@ -487,4 +490,40 @@ export class InkStrokeProperties { } return inkCopy; }); + + @undoBatch + smoothInkStrokes = (inkDocs: Doc[], tolerance: number = 5) => { + inkDocs.forEach(inkDoc => { + const inkView = DocumentView.getDocumentView(inkDoc); + const inkStroke = inkView?.ComponentView as InkingStroke; + const { inkData } = inkStroke.inkScaledData(); + + const result = inkData.length > 2 && GestureUtils.GestureRecognizer.Recognize([inkData]); + console.log(result); + let polygonPoints: { X: number; Y: number }[] | undefined = undefined; + if (result && (result.Name === 'line' ? result.Score > 0.92 : result.Score > 0.85)) { + switch (result.Name) { + case Gestures.Line: + case Gestures.Triangle: + case Gestures.Rectangle: + case Gestures.Circle: + GestureOverlay.makeBezierPolygon(inkData, result.Name, true); + break; + default: + } + } else { + const polylinePoints = inkData.filter((pt, index) => { return index % 4 === 0 || pt === inkData.lastElement()}).map(pt => { return { x: pt.X, y: pt.Y }; }); // prettier-ignore + if (polylinePoints.length > 2) { + const toKeep = simplify(polylinePoints, tolerance).map(pt => {return { X: pt.x, Y: pt.y }}); // prettier-ignore + for (var i = 4; i < inkData.length - 3; i += 4) { + const contains = toKeep.find(pt => pt.X === inkData[i].X && pt.Y === inkData[i].Y); + if (!contains) { + this._currentPoint = i; + inkView && this.deletePoints(inkView, false); + } + } + } + } + }); + }; } diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx index 3f90df7d1..1277a6fe7 100644 --- a/src/client/views/InkTranscription.tsx +++ b/src/client/views/InkTranscription.tsx @@ -6,7 +6,7 @@ import { InkData, InkField, InkTool } from '../../fields/InkField'; import { Cast, DateCast, ImageCast, NumCast, StrCast } from '../../fields/Types'; import { aggregateBounds } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; -import { CollectionFreeFormView } from './collections/collectionFreeForm'; +import { CollectionFreeFormView, MarqueeView } from './collections/collectionFreeForm'; import { InkingStroke } from './InkingStroke'; import './InkTranscription.scss'; import { Docs } from '../documents/Documents'; @@ -387,7 +387,7 @@ export class InkTranscription extends React.Component { ); docView.props.removeDocument?.(selected); // Gets a collection based on the selected nodes using a marquee view ref - const newCollection = marqViewRef?.getCollection(selected, undefined, true); + const newCollection = MarqueeView.getCollection(selected, undefined, true, { left: 1, top: 1, width: 1, height: 1 }); if (newCollection) { newCollection.width = NumCast(newCollection._width); newCollection.height = NumCast(newCollection._height); diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index ce1c07f2f..48b26d903 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -104,10 +104,10 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() * analyzes the ink stroke and saves the analysis of the stroke to the 'inkAnalysis' field, * and the recognized words to the 'handwriting' */ - analyzeStrokes=()=> { + analyzeStrokes = () => { const data: InkData = this.inkScaledData().inkData ?? []; CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ['inkAnalysis', 'handwriting'], [data]); - } + }; /** * Toggles whether the ink stroke is displayed as an overlay mask or as a regular stroke. diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index edf6df2b9..de1800700 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -15,7 +15,7 @@ import { MdClosedCaption, MdClosedCaptionDisabled, MdGridOff, MdGridOn, MdSubtit import { RxWidth } from 'react-icons/rx'; import { TbEditCircle, TbEditCircleOff, TbHandOff, TbHandStop, TbHighlight, TbHighlightOff } from 'react-icons/tb'; import { TfiBarChart } from 'react-icons/tfi'; -import { Doc, Opt } from '../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { DocData } from '../../fields/DocSymbols'; import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, ScriptCast } from '../../fields/Types'; @@ -30,6 +30,8 @@ import { Colors } from './global/globalEnums'; import { DocumentView } from './nodes/DocumentView'; import { OpenWhere } from './nodes/OpenWhere'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; +import { MarqueeOptionsMenu } from './collections/collectionFreeForm'; +import { InkStrokeProperties } from './InkStrokeProperties'; @observer export class PropertiesButtons extends React.Component<{}, {}> { diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss index 840df41e7..aa825a6e9 100644 --- a/src/client/views/PropertiesView.scss +++ b/src/client/views/PropertiesView.scss @@ -638,3 +638,9 @@ padding-left: 8px; background-color: rgb(51, 51, 51); } + +.smooth, +.color, +.smooth-slider { + margin-top: 3px; +} diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index 024db82a4..ac2625f32 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -5,7 +5,7 @@ import { IconLookup } from '@fortawesome/fontawesome-svg-core'; import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Checkbox, Tooltip } from '@mui/material'; -import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components'; +import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Toggle, ToggleType, Type } from 'browndash-components'; import { concat } from 'lodash'; import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; @@ -14,13 +14,13 @@ import { ColorResult, SketchPicker } from 'react-color'; import * as Icons from 'react-icons/bs'; // {BsCollectionFill, BsFillFileEarmarkImageFill} from "react-icons/bs" import { ClientUtils, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; -import { Doc, Field, FieldResult, FieldType, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc'; +import { Doc, DocListCast, Field, FieldResult, FieldType, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast } from '../../fields/Doc'; import { AclAdmin, DocAcl, DocData } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; import { ComputedField } from '../../fields/ScriptField'; -import { Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../fields/Types'; import { GetEffectiveAcl, SharingPermissions, normalizeEmail } from '../../fields/util'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; import { GroupManager } from '../util/GroupManager'; @@ -43,6 +43,10 @@ import { DocumentView } from './nodes/DocumentView'; import { StyleProviderFuncType } from './nodes/FieldView'; import { OpenWhere } from './nodes/OpenWhere'; import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails'; +import { InkingStroke } from './InkingStroke'; +import { SettingsManager } from '../util/SettingsManager'; +import { MarqueeOptionsMenu } from './collections/collectionFreeForm'; +import { SmartDrawHandler } from './smartdraw/SmartDrawHandler'; const _global = (window /* browser */ || global) /* node */ as any; @@ -117,6 +121,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps @observable openAddSlide: boolean = false; @observable openSlideOptions: boolean = false; + // For ink groups + @observable containsInkDoc: boolean = false; + @observable inkDoc: Doc | undefined = undefined; + @observable _controlButton: boolean = false; private _disposers: { [name: string]: IReactionDisposer } = {}; @@ -794,7 +802,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps }; getField(key: string) { - return Field.toString(this.selectedDoc?.[DocData][key] as FieldType); + return this.containsInkDoc ? Field.toString(this.inkDoc?.[DocData][key] as FieldType) : Field.toString(this.selectedDoc?.[DocData][key] as FieldType); } @computed get shapeXps() { return NumCast(this.selectedDoc?.x); } // prettier-ignore @@ -805,8 +813,17 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps set shapeWid(value) { this.selectedDoc && (this.selectedDoc._width = Math.round(value * 100) / 100); } // prettier-ignore @computed get shapeHgt() { return NumCast(this.selectedDoc?._height); } // prettier-ignore set shapeHgt(value) { this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100); } // prettier-ignore - @computed get strokeThk(){ return NumCast(this.selectedDoc?.[DocData].stroke_width); } // prettier-ignore - set strokeThk(value) { this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100); } // prettier-ignore + @computed get strokeThk(){ return this.containsInkDoc ? NumCast(this.inkDoc?.[DocData].stroke_width) : NumCast(this.selectedDoc?.[DocData].stroke_width); } // prettier-ignore + set strokeThk(value) { + if (this.containsInkDoc) { + const childDocs = DocListCast(this.selectedDoc[DocData].data); + childDocs.forEach(doc => { + doc[DocData].stroke_width = Math.round(value * 100) / 100; + }) + } else { + this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100); + } + } // prettier-ignore @computed get hgtInput() { return this.inputBoxDuo( @@ -843,10 +860,32 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps private _lastDash: any = '2'; - @computed get colorFil() { return StrCast(this.selectedDoc?.[DocData].fillColor); } // prettier-ignore - set colorFil(value) { this.selectedDoc && (this.selectedDoc[DocData].fillColor = value || undefined); } // prettier-ignore - @computed get colorStk() { return StrCast(this.selectedDoc?.[DocData].color); } // prettier-ignore - set colorStk(value) { this.selectedDoc && (this.selectedDoc[DocData].color = value || undefined); } // prettier-ignore + @computed get colorFil() { return this.containsInkDoc ? StrCast(this.inkDoc?.[DocData].fillColor) : StrCast(this.selectedDoc?.[DocData].fillColor); } // prettier-ignore + set colorFil(value) { + if (this.containsInkDoc) { + const childDocs = DocListCast(this.selectedDoc[DocData].data); + childDocs.forEach(doc => { + const inkStroke = DocumentView.getDocumentView(doc)?.ComponentView as InkingStroke; + const { inkData } = inkStroke.inkScaledData(); + if (InkingStroke.IsClosed(inkData)) { + doc[DocData].fillColor = value || undefined; + } + }); + } else { + this.selectedDoc && (this.selectedDoc[DocData].fillColor = value || undefined); + } + } + @computed get colorStk() { return this.containsInkDoc ? StrCast(this.inkDoc?.[DocData].color) : StrCast(this.selectedDoc?.[DocData].color); } // prettier-ignore + set colorStk(value) { + if (this.containsInkDoc) { + const childDocs = DocListCast(this.selectedDoc[DocData].data); + childDocs.forEach(doc => { + doc[DocData].color = value || undefined; + }); + } else { + this.selectedDoc && (this.selectedDoc[DocData].color = value || undefined); + } + } colorButton(value: string, type: string, setter: () => void) { return ( @@ -917,10 +956,69 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps ); } - @computed get dashdStk() { return this.selectedDoc?.stroke_dash || ''; } // prettier-ignore + @computed get smoothAndColor() { + const targetDoc = this.selectedLayoutDoc; + return ( + <div> + <div className="smooth"> + <Toggle + text={'Smooth Ink Strokes'} + color={SettingsManager.userColor} + icon={<FontAwesomeIcon icon="bezier-curve" />} + iconPlacement="left" + align="flex-start" + fillWidth + toggleType={ToggleType.BUTTON} + onClick={undoable(() => { + InkStrokeProperties.Instance.smoothInkStrokes(this.containsInkDoc ? DocListCast(targetDoc.data) : [targetDoc], this.smoothAmt); + }, 'smoothStrokes')} + /> + </div> + <div className="smooth-slider"> + {this.getNumber( + 'Smooth Amount', + '', + 1, + Math.max(20, this.smoothAmt), + this.smoothAmt, + (val: number) => { + !isNaN(val) && (this.smoothAmt = val); + }, + 20, + 1 + )} + </div> + {!targetDoc.layout_isSvg && ( + <div className="color"> + <Toggle + text={'Color with GPT'} + color={SettingsManager.userColor} + icon={<FontAwesomeIcon icon="fill-drip" />} + iconPlacement="left" + align="flex-start" + fillWidth + toggleType={ToggleType.BUTTON} + onClick={undoable(() => { + SmartDrawHandler.Instance.colorWithGPT(targetDoc); + }, 'smoothStrokes')} + /> + </div> + )} + </div> + ); + } + + @computed get dashdStk() { return this.containsInkDoc? this.inkDoc?.stroke_dash || '' : this.selectedDoc?.stroke_dash || ''; } // prettier-ignore set dashdStk(value) { value && (this._lastDash = value); - this.selectedDoc && (this.selectedDoc[DocData].stroke_dash = value ? this._lastDash : undefined); + if (this.containsInkDoc) { + const childDocs = DocListCast(this.selectedDoc[DocData].data); + childDocs.forEach(doc => { + doc[DocData].stroke_dash = value ? this._lastDash : undefined; + }); + } else { + this.selectedDoc && (this.selectedDoc[DocData].stroke_dash = value ? this._lastDash : undefined); + } } @computed get widthStk() { return this.getField('stroke_width') || '1'; } // prettier-ignore set widthStk(value) { @@ -930,13 +1028,31 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps set markScal(value) { this.selectedDoc && (this.selectedDoc[DocData].stroke_markerScale = Number(value)); } + @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '1'); } // prettier-ignore + set smoothAmt(value) { + this.selectedDoc && (this.selectedDoc[DocData].stroke_smoothAmount = Number(value)); + } @computed get markHead() { return this.getField('stroke_startMarker') || ''; } // prettier-ignore set markHead(value) { - this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value); + if (this.containsInkDoc) { + const childDocs = DocListCast(this.selectedDoc[DocData].data); + childDocs.forEach(doc => { + doc[DocData].stroke_startMarker = value; + }); + } else { + this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value); + } } @computed get markTail() { return this.getField('stroke_endMarker') || ''; } // prettier-ignore set markTail(value) { - this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value); + if (this.containsInkDoc) { + const childDocs = DocListCast(this.selectedDoc[DocData].data); + childDocs.forEach(doc => { + doc[DocData].stroke_endMarker = value; + }); + } else { + this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value); + } } regInput = (key: string, value: any, setter: (val: string) => {}) => ( @@ -1036,6 +1152,16 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps <div className="appearance-editor"> {this.widthAndDash} {this.strokeAndFill} + {this.smoothAndColor} + </div> + ); + } + + @computed get inkEditor() { + return ( + <div className="ink-editor"> + {this.widthAndDash} + {this.strokeAndFill} </div> ); } @@ -1164,6 +1290,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps } @computed get inkSubMenu() { + this.containsInkDoc = false; return ( // prettier-ignore <> @@ -1177,6 +1304,30 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps ); } + containsInk = (selectedDoc: Doc) => { + const childDocs: Doc[] = DocListCast(selectedDoc[DocData].data); + for (var i = 0; i < childDocs.length; i++) { + if (DocumentView.getDocumentView(childDocs[i])?.layoutDoc?.layout_isSvg) { + this.inkDoc = childDocs[i]; + this.containsInkDoc = true; + return true; + } + } + this.containsInkDoc = false; + return false; + }; + + @computed get inkCollectionSubMenu() { + return ( + // prettier-ignore + <> + <PropertiesSection title="Ink Appearance" isOpen={this.openAppearance} setIsOpen={bool => { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}> + {this.isGroup && this.containsInk(this.selectedDoc) ? this.appearanceEditor : null} + </PropertiesSection> + </> + ); + } + @computed get fieldsSubMenu() { return ( <PropertiesSection @@ -1737,6 +1888,7 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps {this.linksSubMenu} {!this.selectedLink || !this.openLinks ? null : this.linkProperties} {this.inkSubMenu} + {this.inkCollectionSubMenu} {this.contextsSubMenu} {isNovice ? null : this.sharingSubMenu} {this.filtersSubMenu} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index a298e6ac4..8575807b3 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -548,24 +548,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); @@ -652,26 +635,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection 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() - ); + return this.createInkDoc(points); }); newStrokes && this.addDocument?.(newStrokes); // setTimeout(() => this._eraserLock--); @@ -1276,18 +1240,43 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return tVals; }; + 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 = (e: PointerEvent, doubleTap?: boolean) => { - SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createDrawing, this.removeDrawing); + showSmartDraw = (e: PointerEvent) => { + SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc; + SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing; + SmartDrawHandler.Instance.AddDrawing = this.addDrawing; + SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY); }; _drawing: Doc[] = []; _drawingContainer: Doc | undefined = undefined; @undoBatch - createDrawing = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => { + createDrawingDoc = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => { this._drawing = []; const xf = this.screenToFreeformContentsXf; - // this._drawingContainer = undefined; strokeData.forEach((stroke: [InkData, string, string]) => { const bounds = InkField.getBounds(stroke[0]); const B = xf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height); @@ -1310,20 +1299,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ActiveIsInkMask() ); this._drawing.push(inkDoc); - this.addDocument(inkDoc); }); - const collection = this._marqueeViewRef.current?.collection(undefined, true, this._drawing); - if (collection) { - const docData = collection[DocData]; - docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text; - docData.drawingInput = opts.text; - docData.drawingComplexity = opts.complexity; - docData.drawingColored = opts.autoColor; - docData.drawingSize = opts.size; - docData.drawingData = gptRes; - this._drawingContainer = collection; - } - this._batch?.end(); + const collection = MarqueeView.getCollection(this._drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 }); + return collection; }; removeDrawing = (doc?: Doc) => { @@ -1339,6 +1317,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._drawing = []; }; + addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string) => { + const docData = doc[DocData]; + docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text; + 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; @@ -2045,7 +2036,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection optionItems.push({ description: 'Show Drawing Editor', event: action(() => { - !SmartDrawHandler.Instance._showRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10, this.createDrawing, this.removeDrawing) : SmartDrawHandler.Instance.hideRegenerate(); + 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', }); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index b3fdd9379..76c37dff0 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -21,6 +21,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { public pinWithView: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public classifyImages: (e: React.MouseEvent | undefined) => void = unimplementedFunction; public groupImages: () => void = unimplementedFunction; + public smoothStrokes: (docs?: Doc[]) => void = unimplementedFunction; public isShown = () => this._opacity > 0; constructor(props: any) { super(props); @@ -41,6 +42,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { <IconButton tooltip="Delete Documents" onPointerDown={this.delete} icon={<FontAwesomeIcon icon="trash-alt" />} color={this.userColor} /> <IconButton tooltip="Pin selected region" onPointerDown={this.pinWithView} icon={<FontAwesomeIcon icon="map-pin" />} color={this.userColor} /> <IconButton tooltip="Classify Images" onPointerDown={this.classifyImages} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} /> + <IconButton tooltip="Smooth Strokes" onPointerDown={() => this.smoothStrokes} icon={<FontAwesomeIcon icon="object-group" />} color={this.userColor} /> </> ); return this.getElement(buttons); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 8560323c9..92c0da983 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -4,14 +4,14 @@ 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, numberRange } from '../../../../Utils'; +import { intersectRect, numberRange, unimplementedFunction } from '../../../../Utils'; import { Doc, NumListCast, Opt } from '../../../../fields/Doc'; import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkField, InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { RichTextField } from '../../../../fields/RichTextField'; -import { Cast, FieldValue, ImageCast, NumCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, Cast, DocCast, FieldValue, ImageCast, NumCast, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { GetEffectiveAcl } from '../../../../fields/util'; import { gptGetEmbedding, gptImageLabel } from '../../../apis/gpt/GPT'; @@ -26,7 +26,7 @@ import { ContextMenu } from '../../ContextMenu'; import { ObservableReactComponent } from '../../ObservableReactComponent'; import { MarqueeViewBounds } from '../../PinFuncs'; import { PreviewCursor } from '../../PreviewCursor'; -import { DocumentView } from '../../nodes/DocumentView'; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../../nodes/DocumentView'; import { OpenWhere } from '../../nodes/OpenWhere'; import { pasteImageBitmap } from '../../nodes/WebBoxRenderer'; import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; @@ -36,7 +36,12 @@ import { CollectionFreeFormView } from './CollectionFreeFormView'; import { ImageLabelHandler } from './ImageLabelHandler'; import { MarqueeOptionsMenu } from './MarqueeOptionsMenu'; import './MarqueeView.scss'; -import { collectionOf } from '@turf/turf'; +import { collectionOf, points } from '@turf/turf'; +import { InkingStroke } from '../../InkingStroke'; +import { GestureUtils } from '../../../../pen-gestures/GestureUtils'; +import { Gestures } from '../../../../pen-gestures/GestureTypes'; +import { GestureOverlay } from '../../GestureOverlay'; +import { InkStrokeProperties } from '../../InkStrokeProperties'; interface MarqueeViewProps { getContainerTransform: () => Transform; @@ -89,6 +94,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps return bounds; } + public AddInkDoc: (points: InkData) => Doc | void = unimplementedFunction; + componentDidMount() { this._props.setPreviewCursor?.(this.setPreviewCursor); } @@ -277,6 +284,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps MarqueeOptionsMenu.Instance.pinWithView = this.pinWithView; MarqueeOptionsMenu.Instance.classifyImages = this.classifyImages; MarqueeOptionsMenu.Instance.groupImages = this.groupImages; + MarqueeOptionsMenu.Instance.smoothStrokes = this.smoothStrokes; document.addEventListener('pointerdown', hideMarquee, true); document.addEventListener('wheel', hideMarquee, true); } else { @@ -490,6 +498,70 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps }); @undoBatch + smoothStrokes = action((docs?: Doc[]) => { + docs && docs.length > 0 ? (this._selectedDocs = docs) : (this._selectedDocs = this.marqueeSelect(false, DocumentType.INK)); + if (this._selectedDocs.length == 0) return; + + this._selectedDocs.forEach(stroke => { + const docView = DocumentView.getDocumentView(stroke); + const inkStroke = docView?.ComponentView as InkingStroke; + const { inkData } = inkStroke.inkScaledData(); + + const result = inkData.length > 2 && GestureUtils.GestureRecognizer.Recognize([inkData]); + console.log(result); + let polygonPoints: { X: number; Y: number }[] | undefined = undefined; + if (result && (result.Name === 'line' ? result.Score > 0.9 : result.Score > 0.8)) { + switch (result.Name) { + case Gestures.Line: + case Gestures.Triangle: + case Gestures.Rectangle: + case Gestures.Circle: + GestureOverlay.makeBezierPolygon(inkData, result.Name, true); + break; + default: + } + } else { + const distances: number[] = []; + for (var i = 0; i < inkData.length - 3; i += 4) { + distances.push(Math.sqrt((inkData[i].X - inkData[i + 3].X) ** 2 + (inkData[i].Y - inkData[i + 3].Y) ** 2)); + } + const avgDist = (NumCast(stroke.width) + NumCast(stroke.height)) / 2; + // const avgDist = distances.reduce((a, b) => a + b) / distances.length; + if (Math.sqrt((inkData.lastElement().X - inkData[0].X) ** 2 + (inkData.lastElement().Y - inkData[0].Y) ** 2) < avgDist) { + inkData.pop(); + inkData.push({ X: inkData[0].X, Y: inkData[0].Y }); + } + // const editedPoints: InkData = []; + // const toDelete: number[] = []; + + // distances.forEach((dist, i) => { + // if (dist < avgDist / 3) { + // toDelete.unshift(i * 4); + // } + // }); + // toDelete.forEach(pt => { + // InkStrokeProperties.Instance._currentPoint = pt; + // docView && InkStrokeProperties.Instance.deletePoints(docView, false); + // }); + + // for (var i = 0; i < distances.length; i++) { + // if (distances[i] > avgDist / 3) { + // editedPoints.push(...inkData.slice(i * 4, i * 4 + 4)); + // } else { + // if (i !== distances.length) { + // editedPoints.push(...inkData.slice(i * 4, i * 4 + 2)); + // editedPoints.push(...inkData.slice(i * 4 + 6, i * 4 + 8)); + // i++; + // } + // } + // } + // inkData.length = 0; + // inkData.push(...editedPoints); + } + }); + }); + + @undoBatch syntaxHighlight = action((e: KeyboardEvent | React.PointerEvent | undefined) => { const selected = this.marqueeSelect(false); if (e instanceof KeyboardEvent ? e.key === 'i' : true) { diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 9f2a9b8e1..1e700d240 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -295,6 +295,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.DocumentView?.()!, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY); }); + AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => { + const container = DocCast(this._props.Document.embedContainer); + const docView = DocumentView.getDocumentView?.(container); + docView?.ComponentView?._props.addDocument?.(drawing); + drawing.x = NumCast(this._props.Document.x) + (this._props.Document.width as number); + drawing.y = NumCast(this._props.Document.y); + }; + AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? ''); const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to); this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom); diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index df990b0c0..ea574493a 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -15,6 +15,11 @@ import { LinkPopup } from '../linking/LinkPopup'; import { DocumentView } from '../nodes/DocumentView'; import './AnchorMenu.scss'; import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup'; +import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler'; +import { InkData, InkField } from '../../../fields/InkField'; +import { DocData } from '../../../fields/DocSymbols'; +import { undoBatch } from '../../util/UndoManager'; +import ReactLoading from 'react-loading'; @observer export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { @@ -38,6 +43,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { // GPT additions @observable private _selectedText: string = ''; + @observable private _isLoading: boolean = false; @action public setSelectedText = (txt: string) => { this._selectedText = txt.trim(); @@ -61,6 +67,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { public get Active() { return this._left > 0; } + public AddDrawingAnnotation: (doc: Doc) => void = unimplementedFunction; public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined; componentWillUnmount() { @@ -137,6 +144,29 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { this.addToCollection?.(newCol); }; + gptDraw = async (e: React.PointerEvent) => { + try { + SmartDrawHandler.Instance.AddDrawing = this.createDrawingAnnotation; + this._isLoading = true; + await SmartDrawHandler.Instance.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._selectedText, 5, 100, true); + this._isLoading = false; + } catch (err) { + console.error(err); + } + }; + + @undoBatch + createDrawingAnnotation = action((drawing: Doc, opts: DrawingOptions, gptRes: string) => { + this.AddDrawingAnnotation(drawing); + const docData = drawing[DocData]; + docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text; + docData.drawingInput = opts.text; + docData.drawingComplexity = opts.complexity; + docData.drawingColored = opts.autoColor; + docData.drawingSize = opts.size; + docData.drawingData = gptRes; + }); + pointerDown = (e: React.PointerEvent) => { setupMoveUpEvents( this, @@ -239,6 +269,14 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { icon={<FontAwesomeIcon icon="id-card" size="lg" />} color={SettingsManager.userColor} /> + {this._selectedText && ( + <IconButton + tooltip="Create drawing" + onPointerDown={e => this.gptDraw(e)} + icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon="paintbrush" size="lg" />} + color={SettingsManager.userColor} + /> + )} {AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : ( <IconButton tooltip="Click to Record Annotation" // diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index fbe3518ec..5d526c1bf 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -33,7 +33,7 @@ import './PDFViewer.scss'; // pdfjsLib.GlobalWorkerOptions.workerSrc = `/assets/pdf.worker.js`; // The workerSrc property shall be specified. -Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.3.136/build/pdf.worker.mjs'; +Pdfjs.GlobalWorkerOptions.workerSrc = 'https://unpkg.com/pdfjs-dist@4.4.168/build/pdf.worker.mjs'; interface IViewerProps extends FieldViewProps { pdfBox: PDFBox; @@ -435,6 +435,14 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> { GPTPopup.Instance.addDoc = this._props.sidebarAddDoc; // allows for creating collection AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument; + AnchorMenu.Instance.AddDrawingAnnotation = this.addDrawingAnnotation; + }; + + addDrawingAnnotation = (drawing: Doc) => { + // drawing[DocData].x = this._props.pdfBox.ScreenToLocalBoxXf().TranslateX + // const scaleX = this._mainCont.current.offsetWidth / boundingRect.width; + drawing.y = (drawing.y as number) + (this._props.Document.data_sidebar_panY as number); + this._props.addDocument?.(drawing); }; @action diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx index ec4279e3e..7e4d46204 100644 --- a/src/client/views/smartdraw/AnnotationPalette.tsx +++ b/src/client/views/smartdraw/AnnotationPalette.tsx @@ -64,7 +64,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett @action handleKeyPress = async (event: React.KeyboardEvent) => { if (event.key === 'Enter') { - await this.generateDrawing(); + await this.generateDrawings(); } }; @@ -130,15 +130,14 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett } @undoBatch - generateDrawing = action(async () => { + generateDrawings = action(async () => { this._isLoading = true; this._props.Document[DocData].data = undefined; for (var i = 0; i < 3; i++) { try { - SmartDrawHandler.Instance._addFunc = this.createDrawing; + SmartDrawHandler.Instance.AddDrawing = this.addDrawing; this._canInteract = false; if (this._showRegenerate) { - SmartDrawHandler.Instance._deleteFunc = unimplementedFunction; await SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput); } else { await SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor); @@ -154,39 +153,10 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett }); @action - createDrawing = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => { - this._opts = opts; + addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => { this._gptRes.push(gptRes); - const drawing: Doc[] = []; - - strokeList.forEach((stroke: [InkData, string, string]) => { - const bounds = InkField.getBounds(stroke[0]); - const inkWidth = Math.min(5, ActiveInkWidth()); - const inkDoc = Docs.Create.InkDocument( - stroke[0], - { title: 'stroke', - x: bounds.left - inkWidth / 2, - y: bounds.top - inkWidth / 2, - _width: bounds.width + inkWidth, - _height: bounds.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() - ); - drawing.push(inkDoc); - }); - - const collection = MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 }); - if (collection) { - collection[DocData].freeform_fitContentsToBox = true; - Doc.AddDocToList(this._props.Document, 'data', collection); - } + drawing[DocData].freeform_fitContentsToBox = true; + Doc.AddDocToList(this._props.Document, 'data', drawing); }; saveDrawing = async () => { @@ -262,7 +232,7 @@ export class AnnotationPalette extends ObservableReactComponent<AnnotationPalett icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : this._showRegenerate ? <FontAwesomeIcon icon={'rotate'} /> : <AiOutlineSend />} iconPlacement="right" color={SettingsManager.userColor} - onClick={this.generateDrawing} + onClick={this.generateDrawings} /> </div> <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', width: '170px', marginTop: '5px' }}> diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx index c842551c3..52df598ee 100644 --- a/src/client/views/smartdraw/SmartDrawHandler.tsx +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -7,17 +7,23 @@ import { ObservableReactComponent } from '../ObservableReactComponent'; import { Button, IconButton } from 'browndash-components'; import ReactLoading from 'react-loading'; import { AiOutlineSend } from 'react-icons/ai'; -import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT'; -import { InkData, InkTool } from '../../../fields/InkField'; +import { gptAPICall, GPTCallType, gptDrawingColor } from '../../apis/gpt/GPT'; +import { InkData, InkField, InkTool } from '../../../fields/InkField'; import { SVGToBezier } from '../../util/bezierFit'; const { parse } = require('svgson'); import { Slider, Switch } from '@mui/material'; -import { Doc } from '../../../fields/Doc'; +import { Doc, DocListCast } from '../../../fields/Doc'; import { DocData } from '../../../fields/DocSymbols'; -import { DocumentView } from '../nodes/DocumentView'; -import { BoolCast, NumCast, StrCast } from '../../../fields/Types'; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView'; +import { BoolCast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; import './SmartDrawHandler.scss'; import { unimplementedFunction } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; +import { MarqueeView } from '../collections/collectionFreeForm'; +import { ImageField, URLField } from '../../../fields/URLField'; +import { CollectionCardView } from '../collections/CollectionCardDeckView'; +import { InkingStroke } from '../InkingStroke'; +import { undoBatch } from '../../util/UndoManager'; export interface DrawingOptions { text: string; @@ -46,13 +52,43 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> { @observable private _autoColor: boolean = true; @observable private _regenInput: string = ''; @observable private _canInteract: boolean = true; - public _addFunc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void = () => {}; - public _deleteFunc: (doc?: Doc) => void = () => {}; + private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 }; private _lastResponse: string = ''; private _selectedDoc: Doc | undefined = undefined; private _errorOccurredOnce = false; + public RemoveDrawing: (doc?: Doc) => void = unimplementedFunction; + public CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => { + const drawing: Doc[] = []; + strokeList.forEach((stroke: [InkData, string, string]) => { + const bounds = InkField.getBounds(stroke[0]); + const inkWidth = Math.min(5, ActiveInkWidth()); + const inkDoc = Docs.Create.InkDocument( + stroke[0], + { title: 'stroke', + x: bounds.left - inkWidth / 2, + y: bounds.top - inkWidth / 2, + _width: bounds.width + inkWidth, + _height: bounds.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() + ); + drawing.push(inkDoc); + }); + + const collection = MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 }); + return collection; + }; + public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction; + constructor(props: any) { super(props); makeObservable(this); @@ -90,25 +126,26 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> { }; @action - displaySmartDrawHandler = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => { + setEdit = () => { + this._showEditBox = !this._showEditBox; + }; + + @action + displaySmartDrawHandler = (x: number, y: number) => { this._pageX = x; this._pageY = y; this._display = true; - this._addFunc = addFunc; - this._deleteFunc = deleteFunc; }; @action - displayRegenerate = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => { + displayRegenerate = (x: number, y: number) => { this._selectedDoc = DocumentView.SelectedDocs()?.lastElement(); - const docData = this._selectedDoc[DocData]; - this._addFunc = addFunc; - this._deleteFunc = deleteFunc; this._pageX = x; this._pageY = y; this._display = false; this._showRegenerate = true; this._showEditBox = false; + const docData = this._selectedDoc[DocData]; this._lastResponse = StrCast(docData.drawingData); this._lastInput = { text: StrCast(docData.drawingInput), complexity: NumCast(docData.drawingComplexity), size: NumCast(docData.drawingSize), autoColor: BoolCast(docData.drawingColored), x: this._pageX, y: this._pageY }; }; @@ -155,8 +192,8 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> { this._showOptions = false; try { await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor); - this._showRegenerate = true; this.hideSmartDrawHandler(); + this._showRegenerate = true; } catch (err) { if (this._errorOccurredOnce) { console.error('GPT call failed', err); @@ -181,17 +218,15 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> { return; } console.log(res); - const strokeData = await this.parseResponse(res, startPt, false, autoColor); + const strokeData = await this.parseSvg(res, startPt, false, autoColor); + const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData?.data, strokeData?.lastInput, strokeData?.lastRes); + drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); + this._errorOccurredOnce = false; return strokeData; }; @action - edit = () => { - this._showEditBox = !this._showEditBox; - }; - - @action regenerate = async (lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => { if (lastInput) this._lastInput = lastInput; if (lastResponse) this._lastResponse = lastResponse; @@ -211,14 +246,18 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> { return; } console.log(res); - await this.parseResponse(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor); + const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor); + this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(this._selectedDoc); + const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData?.data, strokeData?.lastInput, strokeData?.lastRes); + drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res); + return strokeData; } catch (err) { - console.error('GPT call failed', err); + console.error('Error regenerating drawing', err); } }; @action - parseResponse = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => { + parseSvg = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => { const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g); if (svg) { this._lastResponse = svg[0]; @@ -235,16 +274,70 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> { (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : undefined, ]); }); - if (regenerate) { - if (this._deleteFunc !== unimplementedFunction) this._deleteFunc(this._selectedDoc); - this._addFunc(strokeData, this._lastInput, svg[0]); - } else { - this._addFunc(strokeData, this._lastInput, svg[0]); - } return { data: strokeData, lastInput: this._lastInput, lastRes: svg[0] }; } }; + colorWithGPT = async (drawing: Doc) => { + const img = await this.getIcon(drawing); + const { href } = (img as URLField).url; + const hrefParts = href.split('.'); + const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`; + try { + const hrefBase64 = await CollectionCardView.imageUrlToBase64(hrefComplete); + const strokes = DocListCast(drawing[DocData].data); + const coords: string[] = []; + strokes.forEach((stroke, i) => { + const inkingStroke = DocumentView.getDocumentView(stroke)?.ComponentView as InkingStroke; + const { inkData } = inkingStroke.inkScaledData(); + coords.push( + `${i + 1}. ${inkData + .filter((point, index) => { + return index % 4 === 0 || index == inkData.length - 1; + }) + .map(point => { + return `(${point.X.toString()}, ${point.Y.toString()})`; + })}` + ); + }); + const response = await gptDrawingColor(hrefBase64, coords); + console.log(response); + const colorResponse = await gptAPICall(response, GPTCallType.COLOR, undefined); + console.log(colorResponse); + this.colorStrokes(colorResponse, drawing); + } catch (error) { + console.log('GPT call failed'); + } + }; + + @undoBatch + colorStrokes = (res: string, drawing: Doc) => { + const colorList = res.match(/\{.*?\}/g); + const strokes = DocListCast(drawing[DocData].data); + colorList?.forEach((colors, index) => { + const strokeAndFill = colors.match(/#[0-9A-Fa-f]{6}/g); + if (strokeAndFill && strokeAndFill.length == 2) { + strokes[index][DocData].color = strokeAndFill[0]; + const inkStroke = DocumentView.getDocumentView(strokes[index])?.ComponentView as InkingStroke; + const { inkData } = inkStroke.inkScaledData(); + if (InkingStroke.IsClosed(inkData)) { + strokes[index][DocData].fillColor = strokeAndFill[1]; + } + } + }); + }; + + async getIcon(doc: Doc) { + const docView = DocumentView.getDocumentView(doc); + console.log(doc); + if (docView) { + console.log(docView); + docView.ComponentView?.updateIcon?.(); + return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000)); + } + return undefined; + } + render() { if (this._display) { return ( @@ -397,7 +490,7 @@ export class SmartDrawHandler extends ObservableReactComponent<{}> { color={SettingsManager.userColor} onClick={this.handleSendClick} /> - <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.edit} /> + <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.setEdit} /> {this._showEditBox && ( <div style={{ |