diff options
Diffstat (limited to 'src')
22 files changed, 446 insertions, 299 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index 0590c6930..724725c23 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -1,4 +1,3 @@ -/* eslint-disable @typescript-eslint/no-namespace */ import * as uuid from 'uuid'; export function clamp(n: number, lower: number, upper: number) { @@ -205,7 +204,6 @@ export function intersectRect(r1: { left: number; top: number; width: number; he } export function stringHash(s?: string) { - // eslint-disable-next-line no-bitwise return !s ? undefined : Math.abs(s.split('').reduce((a, b) => (n => n & n)((a << 5) - a + b.charCodeAt(0)), 0)); } @@ -254,6 +252,7 @@ export namespace JSONUtils { try { results = JSON.parse(source); } catch (e) { + console.log('JSONparse error: ', e); results = source; } return results; diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index ba1bc8b7a..bf72a4bd4 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -319,11 +319,11 @@ export class DocumentOptions { contextMenuLabels?: List<string>; contextMenuIcons?: List<string>; childContentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents - childFilters_boolean?: string; + childFilters_boolean?: STRt = new StrInfo('boolean operator to apply to filters on different metadata fields. Value should be AND or OR. Default is AND'); childFilters?: List<string>; childLimitHeight?: NUMt = new NumInfo('whether to limit the height of collection children. 0 - means height can be no bigger than width', false); childLayoutTemplate?: Doc; // template for collection to use to render its children (see PresBox layout in tree view) - childLayoutString?: string; // template string for collection to use to render its children + childLayoutString?: STRt = new StrInfo('JSX layout string for rendering children of a (collection) Doc'); // template string for collection to use to render its children childDocumentsActive?: BOOLt = new BoolInfo('whether child documents are active when parent is document active'); childLayoutFitWidth?: BOOLt = new BoolInfo("whether a child doc's fitWith should be overriden by collection"); childDontRegisterViews?: BOOLt = new BoolInfo('whether child document views should be registered so that they can be found when following links, etc'); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index f296d26bd..05f5621a3 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -743,7 +743,7 @@ pie title Minerals in my tap water { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Vcenter", toolTip: "Vertical center", btnType: ButtonType.ToggleButton, icon: "pallet", toolType:"vcent", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, - { title: "Align", toolTip: "Alignment", btnType: ButtonType.MultiToggleButton, toolType:"alignment", ignoreClick: true, + { title: "Align", toolTip: "Alignment", btnType: ButtonType.MultiToggleButton, toolType:"alignment",ignoreClick: true, subMenu: [ { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} }, @@ -806,7 +806,7 @@ pie title Minerals in my tap water { title: "Rotate",toolTip: "Rotate 90", btnType: ButtonType.ClickButton, icon: "redo-alt", scripts: { onClick: 'imageRotate90();' }}, ]; } - static contextMenuTools():Button[] { + static contextMenuTools(doc:Doc):Button[] { return [ { btnList: new List<string>([CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Tree, CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Multicolumn, @@ -823,6 +823,7 @@ pie title Minerals in my tap water { title: "Num", icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}}, { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, + { title: "Filter", icon: "=", toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.tagGroupTools(),ignoreClick:true, toolType:DocumentType.COL, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor}, { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: {hidden: `IsExploreMode()`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available @@ -835,8 +836,7 @@ pie title Minerals in my tap water { title: "Video", icon: "Video", toolTip: "Video functions", subMenu: CurrentUserUtils.videoTools(), expertMode: false, toolType:DocumentType.VID, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected { title: "Image", icon: "Image", toolTip: "Image functions", subMenu: CurrentUserUtils.imageTools(), expertMode: false, toolType:DocumentType.IMG, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected { title: "Schema", icon: "Schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, linearBtnWidth:58 }, // Only when Schema is selected - { title: "Filter", icon: "=", toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.tagGroupTools(),ignoreClick:true, toolType:DocumentType.COL, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 150}, - ]; + ]; } /// initializes a context menu button for the top bar context menu @@ -881,7 +881,7 @@ pie title Minerals in my tap water static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") { const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsOpen"]), flexGap: 0, childDragAction: dropActionType.embed, childDontRegisterViews: true, linearView_IsOpen: true, ignoreClick: true, linearView_Expandable: false, _height: 35 }; const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined); - const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) ); + const ctxtMenuBtns = CurrentUserUtils.contextMenuTools(doc).map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) ); return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns); } /// Initializes all the default buttons for the top bar context menu diff --git a/src/client/views/ContextMenu.tsx b/src/client/views/ContextMenu.tsx index 5edb5fc0d..399e8a238 100644 --- a/src/client/views/ContextMenu.tsx +++ b/src/client/views/ContextMenu.tsx @@ -252,7 +252,7 @@ export class ContextMenu extends ObservableReactComponent<{ noexpand?: boolean } this._selectedIndex--; } e.preventDefault(); - } else if (e.key === 'Enter' || e.key === 'Tab') { + } else if ((e.key === 'Enter' || e.key === 'Tab') && this._selectedIndex >= 0) { const item = this.flatItems[this._selectedIndex]; if (item.event) { item.event({ x: this.pageX, y: this.pageY }); diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 7f6cfbb87..df9ec1bbf 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@mui/material'; import { IconButton } from 'browndash-components'; -import { action, computed, makeObservable, observable, runInAction } from 'mobx'; +import { action, computed, makeObservable, observable, runInAction, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaUndo } from 'react-icons/fa'; @@ -59,6 +59,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora private _interactionLock?: boolean; @observable _showNothing = true; + @observable private _forceRender = 0; @observable private _accumulatedTitle = ''; @observable private _titleControlString: string = '$title'; @observable private _editingTitle = false; @@ -625,8 +626,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora }); }; - @computed - get selectionTitle(): string { + @computed get selectionTitle(): string { if (DocumentView.Selected().length === 1) { const selected = DocumentView.Selected()[0]; if (this._titleControlString.startsWith('$')) { @@ -647,8 +647,8 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora } return this._rotCenter; } - render() { + this._forceRender; const { b, r, x, y } = this.Bounds; const seldocview = DocumentView.Selected().lastElement(); if (SnappingManager.IsDragging || r - x < 1 || x === Number.MAX_VALUE || !seldocview || this._hidden || isNaN(r) || isNaN(b) || isNaN(x) || isNaN(y)) { @@ -661,6 +661,11 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora return null; } + if (seldocview && !seldocview?.ContentDiv?.getBoundingClientRect().width) { + setTimeout(action(() => this._forceRender++)); // if the selected Doc has no width, then assume it's stil being layed out and try to render again later. + return null; + } + // sharing const acl = GetEffectiveAcl(!this._showLayoutAcl ? Doc.GetProto(seldocview.Document) : seldocview.Document); const docShareMode = HierarchyMapping.get(acl)!.name; diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index befd19f6e..5fddaec9a 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -3,34 +3,40 @@ import { action, computed, makeObservable, observable, runInAction } from 'mobx' import { observer } from 'mobx-react'; import * as React from 'react'; import { returnEmptyFilter, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../ClientUtils'; -import { emptyFunction } from '../../Utils'; +import { emptyFunction, intersectRect } from '../../Utils'; import { Doc, Opt, returnEmptyDoclist } from '../../fields/Doc'; import { InkData, InkField, InkTool } from '../../fields/InkField'; import { NumCast } from '../../fields/Types'; +import { Gestures } from '../../pen-gestures/GestureTypes'; +import { GestureUtils } from '../../pen-gestures/GestureUtils'; +import { Result } from '../../pen-gestures/ndollar'; +import { DocumentType } from '../documents/DocumentTypes'; +import { Docs } from '../documents/Documents'; +import { InteractionUtils } from '../util/InteractionUtils'; +import { ScriptingGlobals } from '../util/ScriptingGlobals'; +import { Transform } from '../util/Transform'; +import { undoable } from '../util/UndoManager'; +import './GestureOverlay.scss'; +import { InkingStroke } from './InkingStroke'; +import { ObservableReactComponent } from './ObservableReactComponent'; +import { returnEmptyDocViewList } from './StyleProvider'; +import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { ActiveArrowEnd, ActiveArrowScale, ActiveArrowStart, ActiveDash, + ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, + DocumentView, SetActiveArrowStart, SetActiveDash, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, } from './nodes/DocumentView'; -import { Gestures } from '../../pen-gestures/GestureTypes'; -import { GestureUtils } from '../../pen-gestures/GestureUtils'; -import { InteractionUtils } from '../util/InteractionUtils'; -import { ScriptingGlobals } from '../util/ScriptingGlobals'; -import { Transform } from '../util/Transform'; -import './GestureOverlay.scss'; -import { ObservableReactComponent } from './ObservableReactComponent'; -import { returnEmptyDocViewList } from './StyleProvider'; -import { ActiveFillColor, DocumentView } from './nodes/DocumentView'; - export enum ToolglassTools { InkToText = 'inktotext', IgnoreGesture = 'ignoregesture', @@ -41,6 +47,10 @@ interface GestureOverlayProps { isActive: boolean; } @observer +/** + * class for gestures. will determine if what the user drew is a gesture, and will transform the ink stroke into the shape the user + * drew or perform the gesture's action + */ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChildren<GestureOverlayProps>> { // eslint-disable-next-line no-use-before-define static Instance: GestureOverlay; @@ -70,10 +80,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil } private _overlayRef = React.createRef<HTMLDivElement>(); - private _d1: Doc | undefined; - private _inkToTextDoc: Doc | undefined; - private thumbIdentifier?: number; - private pointerIdentifier?: number; constructor(props: GestureOverlayProps) { super(props); @@ -88,7 +94,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil componentDidMount() { GestureOverlay.Instance = this; } - @action onPointerDown = (e: React.PointerEvent) => { if (!(e.target as HTMLElement)?.className?.toString().startsWith('lm_')) { @@ -127,78 +132,241 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil // SetActiveArrowEnd('none'); } } + /** + * If what the user drew is a scribble, this returns the documents that were scribbled over + * I changed it so it doesnt use triangles. It will modify an intersect array, with its length being + * how many sharp cusps there are. The first index will have a boolean that is based on if there is an + * intersection in the first 1/length percent of the stroke. The second index will be if there is an intersection + * in the 2nd 1/length percent of the stroke. This array will be used in determineIfScribble(). + * @param ffview freeform view where scribble is drawn + * @param scribbleStroke scribble stroke in screen space coordinats + * @returns array of documents scribbled over + */ + isScribble = (ffView: CollectionFreeFormView, cuspArray: { X: number; Y: number }[], scribbleStroke: { X: number; Y: number }[]) => { + const intersectArray = cuspArray.map(() => false); + const scribbleBounds = InkField.getBounds(scribbleStroke); + const docsToDelete = ffView.childDocs + .map(doc => DocumentView.getDocumentView(doc)) + .filter(dv => dv?.ComponentView instanceof InkingStroke) + .map(dv => dv?.ComponentView as InkingStroke) + .filter(otherInk => { + const otherScreenPts = otherInk.inkScaledData?.().inkData.map(otherInk.ptToScreen); + if (intersectRect(InkField.getBounds(otherScreenPts), scribbleBounds)) { + const intersects = this.findInkIntersections(scribbleStroke, otherScreenPts).map(intersect => { + const percentage = intersect.split('/')[0]; + intersectArray[Math.floor(Number(percentage) * cuspArray.length)] = true; + }); + return intersects.length > 0; + } + }); + return !this.determineIfScribble(intersectArray) ? undefined : + [ ...docsToDelete.map(stroke => stroke.Document), + // bcz: NOTE: docsInBoundingBox test should be replaced with a docsInConvexHull test + ...this.docsInBoundingBox({ topLeft : ffView.ScreenToContentsXf().transformPoint(scribbleBounds.left, scribbleBounds.top), + bottomRight: ffView.ScreenToContentsXf().transformPoint(scribbleBounds.right,scribbleBounds.bottom)}, + ffView.childDocs.filter(doc => !docsToDelete.map(s => s.Document).includes(doc)) )]; // prettier-ignore + }; + /** + * Returns all docs in array that overlap bounds. Note that the bounds should be given in screen space coordinates. + * @param boundingBox screen space bounding box + * @param childDocs array of docs to test against bounding box + * @returns list of docs that overlap rect + */ + docsInBoundingBox = (boundingBox: { topLeft: number[]; bottomRight: number[] }, childDocs: Doc[]): Doc[] => { + const rect = { left: boundingBox.topLeft[0], top: boundingBox.topLeft[1], width: boundingBox.bottomRight[0] - boundingBox.topLeft[0], height: boundingBox.bottomRight[1] - boundingBox.topLeft[1] }; + return childDocs.filter(doc => intersectRect(rect, { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) })); + }; + /** + * Determines if what the array of cusp/intersection data corresponds to a scribble. + * true if there are at least 4 cusps and either: + * 1) the initial and final quarters of the array contain objects + * 2) or half of the cusps contain objects + * @param intersectArray array of booleans coresponding to which scribble sections (regions separated by a cusp) contain Docs + * @returns + */ + determineIfScribble = (intersectArray: boolean[]) => { + const quarterArrayLength = Math.ceil(intersectArray.length / 3.9); // use 3.9 instead of 4 to work better with strokes with only 4 cusps + const { start, end } = intersectArray.reduce((res, val, i) => ({ // test for scribbles at start and end of scribble stroke + start: res.start || (val && i <= quarterArrayLength), + end: res.end || (val && i >= intersectArray.length - quarterArrayLength) + }), { start: false, end: false }); // prettier-ignore + + const percentCuspsWithContent = intersectArray.filter(value => value).length / intersectArray.length; + return intersectArray.length > 3 && (percentCuspsWithContent >= 0.5 || (start && end)); + }; + /** + * determines if inks intersect + * @param line is pointData + * @param triangle triangle with 3 points + * @returns will return an array, with its lenght being equal to how many intersections there are betweent the 2 strokes. + * each item in the array will contain a number between 0-1 or a number 0-1 seperated by a comma. If one of the curves is a line, then + * then there will just be a number that reprents how far that intersection is along the scribble. For example, + * .1 means that the intersection occurs 10% into the scribble, so near the beginning of it. but if they are both curves, then + * it will return two numbers, one for each curve, seperated by a comma. Sometimes, the percentage it returns is inaccurate, + * espcially in the beginning and end parts of the stroke. dont know why. hope this makes sense + */ + findInkIntersections = (scribble: InkData, inkStroke: InkData): string[] => { + const intersectArray: string[] = []; + const scribbleBounds = InkField.getBounds(scribble); + for (let i = 0; i < scribble.length - 3; i += 4) { // for each segment of scribble + for (let j = 0; j < inkStroke.length - 3; j += 4) { // for each segment of ink stroke + const scribbleSeg = InkField.Segment(scribble, i); + const strokeSeg = InkField.Segment(inkStroke, j); + const strokeBounds = InkField.getBounds(strokeSeg.points.map(pt => ({ X: pt.x, Y: pt.y }))); + if (intersectRect(scribbleBounds, strokeBounds)) { + const result = InkField.bintersects(scribbleSeg, strokeSeg)[0]; + if (result !== undefined) { + intersectArray.push(result.toString()); + } + } + } // prettier-ignore + } // prettier-ignore + return intersectArray; + }; + dryInk = () => { + const newPoints = this._points.reduce((p, pts) => { + p.push([pts.X, pts.Y]); + return p; + }, [] as number[][]); + newPoints.pop(); + const controlPoints: { X: number; Y: number }[] = []; + + const bezierCurves = fitCurve.default(newPoints, 10); + Array.from(bezierCurves).forEach(curve => { + controlPoints.push({ X: curve[0][0], Y: curve[0][1] }); + controlPoints.push({ X: curve[1][0], Y: curve[1][1] }); + controlPoints.push({ X: curve[2][0], Y: curve[2][1] }); + controlPoints.push({ X: curve[3][0], Y: curve[3][1] }); + }); + const dist = Math.sqrt((controlPoints[0].X - controlPoints.lastElement().X) * (controlPoints[0].X - controlPoints.lastElement().X) + (controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y)); + if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0]; + this._points.length = 0; + this._points.push(...controlPoints); + this.dispatchGesture(Gestures.Stroke); + }; @action onPointerUp = () => { + const ffView = DocumentView.DownDocView?.ComponentView instanceof CollectionFreeFormView && DocumentView.DownDocView.ComponentView; DocumentView.DownDocView = undefined; if (this._points.length > 1) { const B = this.svgBounds; const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top })); - + const { Name, Score } = + (this.InkShape + ? new Result(this.InkShape, 1, Date.now) + : Doc.UserDoc().recognizeGestures && points.length > 2 + ? GestureUtils.GestureRecognizer.Recognize([points]) + : undefined) ?? + new Result(Gestures.Stroke, 1, Date.now); // prettier-ignore + + const cuspArray = this.getCusps(points); // if any of the shape is activated in the CollectionFreeFormViewChrome - if (this.InkShape) { - GestureOverlay.makeBezierPolygon(this._points, this.InkShape, false); - this.dispatchGesture(this.InkShape); - this.primCreated(); - } - // if we're not drawing in a toolglass try to recognize as gesture - else { - // need to decide when to turn gestures back on - const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize([points]); - let actionPerformed = false; - if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) { - switch (result.Name) { - case Gestures.Line: - case Gestures.Triangle: - case Gestures.Rectangle: - case Gestures.Circle: - GestureOverlay.makeBezierPolygon(this._points, result.Name, true); - actionPerformed = this.dispatchGesture(result.Name); - break; - case Gestures.Scribble: - console.log('scribble'); - break; - case Gestures.RightAngle: - console.log('RightAngle'); - break; - default: { - /* empty */ - } - } + // need to decide when to turn gestures back on + const actionPerformed = ((name: Gestures) => { + switch (name) { + case Gestures.Line: + if (cuspArray.length > 2) return undefined; + // eslint-disable-next-line no-fallthrough + case Gestures.Triangle: + case Gestures.Rectangle: + case Gestures.Circle: + this.makeBezierPolygon(this._points, Name, true); + return this.dispatchGesture(name); + case Gestures.RightAngle: + return ffView && this.convertToText(ffView).length > 0; + default: } - - // if no gesture (or if the gesture was unsuccessful), "dry" the stroke into an ink document - if (!actionPerformed) { - const newPoints = this._points.reduce((p, pts) => { - p.push([pts.X, pts.Y]); - return p; - }, [] as number[][]); - newPoints.pop(); - const controlPoints: { X: number; Y: number }[] = []; - - const bezierCurves = fitCurve.default(newPoints, 10); - Array.from(bezierCurves).forEach(curve => { - controlPoints.push({ X: curve[0][0], Y: curve[0][1] }); - controlPoints.push({ X: curve[1][0], Y: curve[1][1] }); - controlPoints.push({ X: curve[2][0], Y: curve[2][1] }); - controlPoints.push({ X: curve[3][0], Y: curve[3][1] }); - }); - const dist = Math.sqrt( - (controlPoints[0].X - controlPoints.lastElement().X) * (controlPoints[0].X - controlPoints.lastElement().X) + (controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y) - ); - // eslint-disable-next-line prefer-destructuring - if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0]; - this._points.length = 0; - this._points.push(...controlPoints); - this.dispatchGesture(Gestures.Stroke); + })(Score < 0.7 ? Gestures.Stroke : (Name as Gestures)); + // if no gesture (or if the gesture was unsuccessful), "dry" the stroke into an ink document + + if (!actionPerformed) { + const scribbledOver = ffView && this.isScribble(ffView, cuspArray, this._points); + if (scribbledOver) { + undoable(() => ffView.removeDocument(scribbledOver), 'scribble erase')(); + } else { + this.dryInk(); } } } this._points.length = 0; }; - - 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); + /** + * used in the rightAngle gesture to convert handwriting into text. will only work on collections + * TODO: make it work on individual ink docs. + */ + convertToText = (ffView: CollectionFreeFormView) => { + let minX = 999999999; + let maxX = -999999999; + let minY = 999999999; + let maxY = -999999999; + const textDocs: Doc[] = []; + ffView.childDocs + .filter(doc => doc.type === DocumentType.COL) + .forEach(doc => { + if (typeof doc.width === 'number' && typeof doc.height === 'number' && typeof doc.x === 'number' && typeof doc.y === 'number') { + const bounds = DocumentView.getDocumentView(doc)?.getBounds; + if (bounds) { + if (intersectRect({ ...bounds, width: bounds.right - bounds.left, height: bounds.bottom - bounds.top }, InkField.getBounds(this._points))) { + if (doc.x < minX) { + minX = doc.x; + } + if (doc.x > maxX) { + maxX = doc.x; + } + if (doc.y < minY) { + minY = doc.y; + } + if (doc.y + doc.height > maxY) { + maxY = doc.y + doc.height; + } + const newDoc = Docs.Create.TextDocument(doc.transcription as string, { title: '', x: doc.x as number, y: minY }); + newDoc.height = doc.height; + newDoc.width = doc.width; + if (ffView.addDocument && ffView.removeDocument) { + ffView.addDocument(newDoc); + ffView.removeDocument(doc); + } + textDocs.push(newDoc); + } + } + } + }); + return textDocs; + }; + /** + * Returns array of coordinates corresponding to the sharp cusps in an input stroke + * @param points array of X,Y stroke coordinates + * @returns array containing the coordinates of the sharp cusps + */ + getCusps(points: InkData) { + const arrayOfPoints: { X: number; Y: number }[] = []; + arrayOfPoints.push(points[0]); + for (let i = 0; i < points.length - 2; i++) { + const point1 = points[i]; + const point2 = points[i + 1]; + const point3 = points[i + 2]; + if (this.find_angle(point1, point2, point3) < 90) { + // NOTE: this is not an accurate way to find cusps -- it is highly dependent on sampling rate and doesn't work well with slowly drawn scribbles + arrayOfPoints.push(point2); + } + } + arrayOfPoints.push(points[points.length - 1]); + return arrayOfPoints; + } + /** + * takes in three points and then determines the angle of the points. used to determine if the cusp + * is sharp enoug + * @returns + */ + find_angle(A: { X: number; Y: number }, B: { X: number; Y: number }, C: { X: number; Y: number }) { + const AB = Math.sqrt(Math.pow(B.X - A.X, 2) + Math.pow(B.Y - A.Y, 2)); + const BC = Math.sqrt(Math.pow(B.X - C.X, 2) + Math.pow(B.Y - C.Y, 2)); + const AC = Math.sqrt(Math.pow(C.X - A.X, 2) + Math.pow(C.Y - A.Y, 2)); + return Math.acos((BC * BC + AB * AB - AC * AC) / (2 * BC * AB)) * (180 / Math.PI); + } + makeBezierPolygon = (points: { X: number; Y: number }[], shape: string, gesture: boolean) => { + const xs = this._points.map(p => p.X); + const ys = this._points.map(p => p.Y); let right = Math.max(...xs); let left = Math.min(...xs); let bottom = Math.max(...ys); @@ -394,7 +562,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil this._strokes.map((l, i) => { const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(l, true); return ( - // eslint-disable-next-line react/no-array-index-key <svg key={i} width={b.width} height={b.height} style={{ top: 0, left: 0, transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}> {InteractionUtils.CreatePolyline( l, diff --git a/src/client/views/InkTranscription.scss b/src/client/views/InkTranscription.scss index 18d6b8b10..c77117ccc 100644 --- a/src/client/views/InkTranscription.scss +++ b/src/client/views/InkTranscription.scss @@ -2,9 +2,7 @@ .error-msg { display: none !important; } - .ms-editor{ - .smartguide{ - top:1000px; - } + .ms-editor { + top: 1000px; } -}
\ No newline at end of file +} diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx index 26e0aa372..8698a6962 100644 --- a/src/client/views/InkTranscription.tsx +++ b/src/client/views/InkTranscription.tsx @@ -3,7 +3,7 @@ import { action, observable } from 'mobx'; import * as React from 'react'; import { Doc, DocListCast } from '../../fields/Doc'; import { InkData, InkField, InkTool } from '../../fields/InkField'; -import { Cast, DateCast, DocCast, ImageCast, NumCast, StrCast } from '../../fields/Types'; +import { Cast, DateCast, ImageCast, NumCast } from '../../fields/Types'; import { aggregateBounds } from '../../Utils'; import { DocumentType } from '../documents/DocumentTypes'; import { CollectionFreeFormView, MarqueeView } from './collections/collectionFreeForm'; @@ -11,11 +11,8 @@ import { InkingStroke } from './InkingStroke'; import './InkTranscription.scss'; import { Docs } from '../documents/Documents'; import { DocumentView } from './nodes/DocumentView'; -import { Number } from 'mongoose'; -import { NumberArray } from 'd3'; import { ImageField } from '../../fields/URLField'; import { gptHandwriting } from '../apis/gpt/GPT'; -import * as fs from 'fs'; import { URLField } from '../../fields/URLField'; /** * Class component that handles inking in writing mode @@ -23,30 +20,30 @@ import { URLField } from '../../fields/URLField'; export class InkTranscription extends React.Component { static Instance: InkTranscription; + // eslint-disable-next-line @typescript-eslint/no-explicit-any @observable _mathRegister: any = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any @observable _mathRef: any = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any @observable _textRegister: any = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any @observable _textRef: any = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any @observable iinkEditor: any = undefined; + // eslint-disable-next-line @typescript-eslint/no-explicit-any private lastJiix: any; private currGroup?: Doc; private collectionFreeForm?: CollectionFreeFormView; - constructor(props: Readonly<{}>) { + constructor(props: Readonly<object>) { super(props); InkTranscription.Instance = this; } - - componentWillUnmount() { - // this._mathRef.removeEventListener('exported', (e: any) => this.exportInk(e, this._mathRef)); - // this._textRef.removeEventListener('exported', (e: any) => this.exportInk(e, this._textRef)); - } - @action + // eslint-disable-next-line @typescript-eslint/no-explicit-any setMathRef = async (r: any) => { if (!this._textRegister && r) { - let editor; const options = { configuration: { server: { @@ -61,21 +58,22 @@ export class InkTranscription extends React.Component { }, }, }; - - editor = new iink.Editor(r, options as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const editor = new iink.Editor(r, options as any); await editor.initialize(); this._textRegister = r; + // eslint-disable-next-line @typescript-eslint/no-explicit-any r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef)); return (this._textRef = r); } }; @action + // eslint-disable-next-line @typescript-eslint/no-explicit-any setTextRef = async (r: any) => { if (!this._textRegister && r) { - let editor; const options = { configuration: { server: { @@ -90,12 +88,13 @@ export class InkTranscription extends React.Component { }, }, }; - - editor = new iink.Editor(r, options as any); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const editor = new iink.Editor(r, options as any); await editor.initialize(); this.iinkEditor = editor; this._textRegister = r; + // eslint-disable-next-line @typescript-eslint/no-explicit-any r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef)); return (this._textRef = r); @@ -124,28 +123,9 @@ export class InkTranscription extends React.Component { strokes.push(d.inkData.map(pd => inkStroke.ptToScreen({ X: pd.X, Y: pd.Y }))); times.push(DateCast(i.author_date).getDate().getTime()); }); - console.log(strokes); - console.log( - `this.Multistrokes.push( - new Multistroke( - Gestures.Scribble, - useBoundedRotationInvariance, - new Array([ - ` + - this.convertPointsToString(strokes) + - ` - ]) - ) - ) - ` - ); - //console.log(this.convertPointsToString(strokes)); - //console.log(this.convertPointsToString2(strokes)); this.currGroup = groupDoc; const pointerData = strokes.map((stroke, i) => this.inkJSON(stroke, times[i])); - const processGestures = false; if (math) { - console.log('math'); this.iinkEditor.importPointEvents(pointerData); } else { this.iinkEditor.importPointEvents(pointerData); @@ -172,9 +152,9 @@ export class InkTranscription extends React.Component { t: number; p: number; } - let strokeObjects: strokeData[] = []; + const strokeObjects: strokeData[] = []; stroke.forEach(point => { - let tempObject: strokeData = { + const tempObject: strokeData = { x: point.X, y: point.Y, t: time, @@ -220,6 +200,7 @@ export class InkTranscription extends React.Component { * @param e the event objects * @param ref the ref to the editor */ + // eslint-disable-next-line @typescript-eslint/no-explicit-any exportInk = async (e: any, ref: any) => { const exports = e.detail['application/vnd.myscript.jiix']; if (exports) { @@ -236,6 +217,7 @@ export class InkTranscription extends React.Component { this.lastJiix = JSON.parse(exports['application/vnd.myscript.jiix']); // map timestamp to strokes const timestampWord = new Map<number, string>(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any this.lastJiix.words.map((word: any) => { if (word.items) { word.items.forEach((i: { id: string; timestamp: string; X: Array<number>; Y: Array<number>; F: Array<number> }) => { @@ -271,12 +253,7 @@ export class InkTranscription extends React.Component { if (this.currGroup && text) { DocumentView.getDocumentView(this.currGroup)?.ComponentView?.updateIcon?.(); - this.currGroup.transcription = text; - this.currGroup.title = text; - let image = await DocumentView.GetDocImage(this.currGroup); - const pathname = image?.url.href as string; - console.log(image?.url); - console.log(image); + const image = await this.getIcon(); const { href } = (image as URLField).url; const hrefParts = href.split('.'); const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`; @@ -284,11 +261,12 @@ export class InkTranscription extends React.Component { try { const hrefBase64 = await this.imageUrlToBase64(hrefComplete); response = await gptHandwriting(hrefBase64); - console.log(response); - } catch (error) { - console.log('bad things have happened'); + } catch { + console.error('Error getting image'); } const textBoxText = 'iink: ' + text + '\n' + '\n' + 'ChatGPT: ' + response; + this.currGroup.transcription = response; + this.currGroup.title = response; if (!this.currGroup.hasTextBox) { const newDoc = Docs.Create.TextDocument(textBoxText, { title: '', x: this.currGroup.x as number, y: (this.currGroup.y as number) + (this.currGroup.height as number) }); newDoc.height = 200; @@ -300,6 +278,22 @@ export class InkTranscription extends React.Component { } } }; + /** + * gets the icon of the collection that was just made + * @returns the image of the collection + */ + async getIcon() { + const docView = DocumentView.getDocumentView(this.currGroup); + if (docView) { + docView.ComponentView?.updateIcon?.(); + return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000)); + } + return undefined; + } + /** + * converts the image to base url formate + * @param imageUrl imageurl taken from the collection icon + */ imageUrlToBase64 = async (imageUrl: string): Promise<string> => { try { const response = await fetch(imageUrl); @@ -403,8 +397,8 @@ export class InkTranscription extends React.Component { } // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs - newCollection && docView.props.addDocument?.(newCollection); if (newCollection) { + docView.props.addDocument?.(newCollection); newCollection.hasTextBox = false; } return newCollection; @@ -413,8 +407,8 @@ export class InkTranscription extends React.Component { render() { return ( <div className="ink-transcription"> - <div className="math-editor" ref={this.setMathRef} touch-action="none"></div> - <div className="text-editor" ref={this.setTextRef} touch-action="none"></div> + <div className="math-editor" ref={this.setMathRef}></div> + <div className="text-editor" ref={this.setTextRef}></div> </div> ); } diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 32bbd57c6..177400882 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -49,7 +49,7 @@ import { PinDocView, PinProps } from './PinFuncs'; import { StyleProp } from './StyleProp'; import { ViewBoxInterface } from './ViewBoxInterface'; -// eslint-disable-next-line @typescript-eslint/no-var-requires +// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports const { INK_MASK_SIZE } = require('./global/globalCssVariables.module.scss'); // prettier-ignore @observer @@ -331,9 +331,10 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() 1, 1, '' as Gestures, - 'none', + 'all', 1.0, - false + false, + this.onPointerDown )} <InkControlPtHandles inkView={this} inkDoc={inkDoc} inkCtrlPoints={inkData} screenCtrlPoints={this.screenCtrlPts} nearestScreenPt={this.nearestScreenPt} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} /> <InkTangentHandles inkView={this} inkDoc={inkDoc} screenCtrlPoints={this.screenCtrlPts} screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} /> diff --git a/src/client/views/collections/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx index d9c27254a..e5a6ebc7f 100644 --- a/src/client/views/collections/CollectionCardDeckView.tsx +++ b/src/client/views/collections/CollectionCardDeckView.tsx @@ -1,4 +1,4 @@ -import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx'; +import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, trace } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ClientUtils, DashColor, returnFalse, returnZero } from '../../../ClientUtils'; @@ -46,8 +46,9 @@ 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; - @observable _forceChildXf = false; + @observable _forceChildXf = 0; @observable _hoveredNodeIndex = -1; @observable _docRefs = new ObservableMap<Doc, DocumentView>(); @observable _maxRowCount = 10; @@ -99,8 +100,7 @@ export class CollectionCardView extends CollectionSubView() { }; componentDidMount() { - this.Document.childFilters_boolean = 'OR'; // bcz: really shouldn't be assigning to fields from within didMount -- this should be a default/override beahavior somehow - + this._props.setContentViewBox?.(this); // Reaction to cardSort changes this._disposers.sort = reaction( () => GPTPopup.Instance.visible, @@ -112,6 +112,17 @@ export class CollectionCardView extends CollectionSubView() { } } ); + // if card deck moves, then the child doc views are hidden so their screen to local transforms will return empty rectangles + // when inquired from the dom (below in childScreenToLocal). When the doc is actually renders, we need to act like the + // dash data just changed and trigger a React involidation with the correct data (read from the dom). + this._disposers.child = reaction( + () => [this.Document.x, this.Document.y], + () => { + if (!Array.from(this._docRefs.values()).every(dv => dv.ContentDiv?.getBoundingClientRect().width)) { + setTimeout(action(() => this._forceChildXf++)); + } + } + ); } componentWillUnmount() { @@ -176,6 +187,7 @@ export class CollectionCardView extends CollectionSubView() { inactiveDocs = () => this.childDocsWithoutLinks.filter(d => !DocumentView.SelectedDocs().includes(d)); 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(); @@ -267,14 +279,7 @@ export class CollectionCardView extends CollectionSubView() { @action onPointerMove = (x: number, y: number) => { - this._docDraggedIndex = -1; - if (DragManager.docsBeingDragged.length) { - const newIndex = this.findCardDropIndex(x, y); - - if (newIndex !== this._docDraggedIndex && newIndex != -1) { - this._docDraggedIndex = newIndex; - } - } + this._docDraggedIndex = DragManager.docsBeingDragged.length ? this.findCardDropIndex(x, y) : -1; }; /** @@ -306,6 +311,7 @@ export class CollectionCardView extends CollectionSubView() { if (de.complete.docDragData.removeDocument?.(draggedDoc)) { this.dataDoc[this.fieldKey] = new List<Doc>(sorted); } + this._dropped = true; } e.stopPropagation(); return true; @@ -340,7 +346,8 @@ export class CollectionCardView extends CollectionSubView() { * @param isDesc * @returns */ - sort = (docs: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => { + sort = (docsIn: Doc[], sortType: cardSortings, isDesc: boolean, dragIndex: number) => { + const docs = docsIn.slice(); // need make new object list since sort() modifies the incoming list which confuses mobx caching sortType && docs.sort((docA, docB) => { const [typeA, typeB] = (() => { @@ -364,21 +371,21 @@ export class CollectionCardView extends CollectionSubView() { const out = typeA < typeB ? -1 : typeA > typeB ? 1 : 0; return isDesc ? out : -out; }); - if (dragIndex != -1) { + if (dragIndex !== -1) { const draggedDoc = DragManager.docsBeingDragged[0]; const originalIndex = docs.findIndex(doc => doc === draggedDoc); originalIndex !== -1 && docs.splice(originalIndex, 1); - docs.splice(dragIndex, 0, draggedDoc); + draggedDoc && docs.splice(dragIndex, 0, draggedDoc); } - return [...docs]; // need to spread docs into a new object list since sort() modifies the incoming list which confuses mobx caching + return docs; }; displayDoc = (doc: Doc, screenToLocalTransform: () => Transform) => ( <DocumentView {...this._props} - ref={action((r: DocumentView) => r?.ContentDiv && this._docRefs.set(doc, r))} + ref={action((r: DocumentView) => (!r?.ContentDiv ? this._docRefs.delete(doc) : this._docRefs.set(doc, r)))} Document={doc} NativeWidth={returnZero} NativeHeight={returnZero} @@ -387,11 +394,12 @@ export class CollectionCardView extends CollectionSubView() { renderDepth={this._props.renderDepth + 1} LayoutTemplate={this._props.childLayoutTemplate} 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._props.PanelHeight() * this.fitContentScale} + 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} @@ -593,7 +601,10 @@ export class CollectionCardView extends CollectionSubView() { 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, @@ -608,25 +619,30 @@ export class CollectionCardView extends CollectionSubView() { return (rowCenterIndex - indexInRow) * 100 - 50; }; const aspect = NumCast(doc.height) / NumCast(doc.width, 1); - const vscale = Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale) / (aspect * this.childPanelWidth()), - (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10))); // prettier-ignore + const vscale = Math.max(1,Math.min((this._props.PanelHeight() * 0.95 * this.fitContentScale) / (aspect * this.childPanelWidth()), + (this._props.PanelHeight() - 80) / (aspect * (this._props.PanelWidth() / 10)))); // prettier-ignore const hscale = Math.min(this.sortedDocs.length, this._maxRowCount) / 2; // bcz: hack - the grid is divided evenly into maxRowCount cells, so the max scaling would be maxRowCount -- but making things that wide is ugly, so cap it off at half the window size return ( <div key={doc[Id]} className={`card-item${isSelected ? '-active' : anySelected ? '-inactive' : ''}`} - onPointerUp={() => { - if (DocumentView.SelectedDocs().includes(doc)) return; - // this turns off documentDecorations during a transition, then turns them back on afterward. - SnappingManager.SetIsResizing(doc[Id]); - setTimeout( - action(() => { - SnappingManager.SetIsResizing(undefined); - this._forceChildXf = !this._forceChildXf; - }), - 900 - ); - }} + 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 + ); + } + })} style={{ width: this.childPanelWidth(), height: 'max-content', @@ -635,7 +651,7 @@ export class CollectionCardView extends CollectionSubView() { rotate(${!isSelected ? this.rotate(amCards, calcRowIndex) : 0}deg) scale(${isSelected ? `${Math.min(hscale, vscale) * 100}%` : this._hoveredNodeIndex === index ? 1.05 : 1})`, }} // prettier-ignore - onMouseEnter={() => this.setHoveredNodeIndex(index)}> + onPointerEnter={() => !SnappingManager.IsDragging && this.setHoveredNodeIndex(index)}> {this.displayDoc(doc, childScreenToLocal)} </div> ); @@ -649,7 +665,8 @@ export class CollectionCardView extends CollectionSubView() { <div className="collectionCardView-outer" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} - onPointerMove={e => this.onPointerMove(e.clientX, e.clientY)} + onPointerLeave={action(() => (this._docDraggedIndex = -1))} + onPointerMove={e => this.onPointerMove(...this._props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY))} onDrop={this.onExternalDrop.bind(this)} style={{ background: this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string, diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index 4951590dc..cf86a0a4e 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -14,6 +14,7 @@ import { DocumentView } from '../nodes/DocumentView'; import { FocusViewOptions } from '../nodes/FocusViewOptions'; import './CollectionCarousel3DView.scss'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; +import { Transform } from '../../util/Transform'; const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss'); @@ -45,15 +46,32 @@ export class CollectionCarousel3DView extends CollectionSubView() { } centerScale = Number(CAROUSEL3D_CENTER_SCALE); + sideScale = Number(CAROUSEL3D_SIDE_SCALE); panelWidth = () => this._props.PanelWidth() / 3; - panelHeight = () => this._props.PanelHeight() * Number(CAROUSEL3D_SIDE_SCALE); + panelHeight = () => this._props.PanelHeight() * this.sideScale; onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); isContentActive = () => this._props.isSelected() || this._props.isContentActive() || this._props.isAnyChildContentActive(); isChildContentActive = () => !!this.isContentActive(); - childScreenToLocal = () => - this._props // document's left is the panel shifted by the doc's index * panelWidth/#docs. But it scales by centerScale around its center, so it's left moves left by the distance of the left from the center (panelwidth/2) * the scale delta (centerScale-1) - .ScreenToLocalTransform() // the top behaves the same way ecept it's shifted by the 'top' amount specified for the panel in css and then by the scale factor. - .translate(-this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2) + childScreenLeftToLocal = () => + this._props + .ScreenToLocalTransform() + .scale(this._props.NativeDimScaling?.() || 1) + .translate(-(this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + .scale(1 / this.sideScale); + childScreenRightToLocal = () => + this._props + .ScreenToLocalTransform() + .scale(this._props.NativeDimScaling?.() || 1) + .translate(-2 * this.panelWidth() - (this.panelWidth() - this.panelWidth() * this.sideScale) / 2, -(this.panelHeight() - this.panelHeight() * this.sideScale) / 2 - (Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + .scale(1 / this.sideScale); + childCenterScreenToLocal = () => + this._props + .ScreenToLocalTransform() + .scale(this._props.NativeDimScaling?.() || 1) + .translate( + -this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, // Focused Doc is shifted right by 1/3 panel width then left by increased size percent of center * 1/2 * panel width / 3 + -((Number(CAROUSEL3D_TOP) / 100) * this._props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2 + ) // top is top margin % of panelHeight - increased size percent of center * panelHeight / 2 .scale(1 / this.centerScale); focus = (anchor: Doc, options: FocusViewOptions): Opt<number> => { @@ -65,9 +83,10 @@ export class CollectionCarousel3DView extends CollectionSubView() { index !== -1 && (this.layoutDoc._carousel_index = index); return undefined; }; + @computed get content() { const currentIndex = NumCast(this.layoutDoc._carousel_index); - const displayDoc = (childPair: { layout: Doc; data: Doc }) => ( + const displayDoc = (childPair: { layout: Doc; data: Doc }, dxf: () => Transform) => ( <DocumentView // eslint-disable-next-line react/jsx-props-no-spreading {...this._props} @@ -83,7 +102,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { LayoutTemplate={this._props.childLayoutTemplate} LayoutTemplateString={this._props.childLayoutString} focus={this.focus} - ScreenToLocalTransform={this.childScreenToLocal} + ScreenToLocalTransform={dxf} isContentActive={this.isChildContentActive} isDocumentActive={this._props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this._props.isDocumentActive : this.isContentActive} PanelWidth={this.panelWidth} @@ -93,7 +112,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { return this.carouselItems.map((childPair, index) => ( <div key={childPair.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}> - {displayDoc(childPair)} + {displayDoc(childPair, index < currentIndex ? this.childScreenLeftToLocal : index === currentIndex ? this.childCenterScreenToLocal : this.childScreenRightToLocal)} </div> )); } diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 9741c45fe..143cfe0e8 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -71,7 +71,7 @@ export class CollectionCarouselView extends CollectionSubView() { @computed get marginX() { return NumCast(this.layoutDoc.caption_xMargin, 50); } // prettier-ignore @computed get carouselIndex() { return NumCast(this.layoutDoc._carousel_index) % this.carouselItems.length; } // prettier-ignore @computed get carouselItems() { - return DocListCast(this.childDocList) + return this.childDocs .filter(doc => doc.type !== DocumentType.LINK) .filter(doc => { switch (StrCast(this.layoutDoc.filterOp)) { @@ -155,6 +155,8 @@ export class CollectionCarouselView extends CollectionSubView() { ? false : undefined; + childScreenToLocalXf = () => this._props.ScreenToLocalTransform().scale(this._props.NativeDimScaling?.() || 1); + renderDoc = (doc: Doc, showCaptions: boolean, overlayFunc?: (r: DocumentView | null) => void) => { return ( <DocumentView @@ -164,8 +166,10 @@ export class CollectionCarouselView extends CollectionSubView() { NativeWidth={returnZero} NativeHeight={returnZero} fitWidth={undefined} + showTags={true} containerViewPath={this.childContainerViewPath} setContentViewBox={undefined} + ScreenToLocalTransform={this.childScreenToLocalXf} onDoubleClickScript={this.onContentDoubleClick} onClickScript={this.onContentClick} isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive} @@ -245,9 +249,6 @@ export class CollectionCarouselView extends CollectionSubView() { <div key="fwd" className="carouselView-fwd" onClick={this.advance}> <FontAwesomeIcon icon="chevron-right" size="2x" /> </div> - <div key="star" className="carouselView-star" onClick={this.star}> - <FontAwesomeIcon icon="star" color={TagItem.docHasTag(this.carouselItems?.[this.carouselIndex], this.starField) ? 'yellow' : 'gray'} size="1x" /> - </div> <div key="remove" className="carouselView-remove" onClick={e => this.setPracticeVal(e, practiceVal.MISSED)} style={{ visibility: this.layoutDoc.filterOp === cardMode.PRACTICE ? 'visible' : 'hidden' }}> <FontAwesomeIcon icon="xmark" color="red" size="1x" /> </div> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index ccce2662a..fe5ced29e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -857,13 +857,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) { @@ -1137,26 +1136,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. @@ -1188,7 +1167,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). @@ -2033,6 +2012,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' }); }; diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index f54d8311d..b24fca8e2 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -90,7 +90,7 @@ export class LinkMenuItem extends ObservableReactComponent<LinkMenuItemProps> { moveEv => { const dragData = new DragManager.DocumentDragData([this._props.linkDoc], dropActionType.embed); dragData.dropPropertiesToRemove = ['hidden']; - DragManager.StartDocumentDrag([this._editRef.current!], dragData, moveEv.x, moveEv.y); + DragManager.StartDocumentDrag([this._editRef.current!], dragData, moveEv.x, moveEv.y, undefined, e => (this._props.linkDoc._layout_isSvg = true)); return true; }, emptyFunction, diff --git a/src/client/views/nodes/DiagramBox.scss b/src/client/views/nodes/DiagramBox.scss index 323638bff..8a7863c14 100644 --- a/src/client/views/nodes/DiagramBox.scss +++ b/src/client/views/nodes/DiagramBox.scss @@ -1,5 +1,3 @@ -$searchbarHeight: 50px; - .DIYNodeBox { width: 100%; height: 100%; @@ -23,7 +21,6 @@ $searchbarHeight: 50px; justify-content: center; align-items: center; width: 100%; - height: $searchbarHeight; padding: 10px; input[type='text'] { @@ -42,7 +39,6 @@ $searchbarHeight: 50px; justify-content: center; align-items: center; width: 100%; - height: calc(100% - $searchbarHeight); .diagramBox { flex: 1; display: flex; diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx index 36deb2d8d..d6c9bb013 100644 --- a/src/client/views/nodes/DiagramBox.tsx +++ b/src/client/views/nodes/DiagramBox.tsx @@ -18,7 +18,9 @@ import { InkingStroke } from '../InkingStroke'; import './DiagramBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; - +/** + * this is a class for the diagram box doc type that can be found in the tools section of the side bar + */ @observer export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { public static LayoutString(fieldKey: string) { @@ -59,14 +61,21 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { { fireImmediately: true } ); } + /** + * helper method for renderMermaidAsync + * @param str string containing the mermaid code + * @returns + */ renderMermaid = (str: string) => { try { return mermaid.render('graph' + Date.now(), str); - } catch (error) { + } catch { return { svg: '', bindFunctions: undefined }; } }; - + /** + * will update the div containing the mermaid diagram to render the new mermaidCode + */ renderMermaidAsync = async (mermaidCode: string, dashDiv: HTMLDivElement) => { try { const { svg, bindFunctions } = await this.renderMermaid(mermaidCode); @@ -97,7 +106,9 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { res ); }, 'set mermaid code'); - + /** + * will generate mermaid code with GPT based on what the user requested + */ generateMermaidCode = action(() => { this._generating = true; const prompt = 'Write this in mermaid code and only give me the mermaid code: ' + this._inputValue; diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx index cb0c4d188..aa40b14aa 100644 --- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx +++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx @@ -265,6 +265,7 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { const toggleStatus = script?.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result as boolean; // Colors const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string; + const background = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string; const items = DocListCast(this.dataDoc.data); const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType)); return ( @@ -272,11 +273,11 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() { tooltip={`Toggle ${tooltip}`} type={Type.PRIM} color={color} + background={background === SnappingManager.userBackgroundColor ? undefined : background} multiSelect={true} onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))} isToggle={script ? true : false} toggleStatus={toggleStatus} - //background={SnappingManager.userBackgroundColor} label={this.label} items={items.map(item => ({ icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as IconProp} color={color} />, diff --git a/src/client/views/nodes/IconTagBox.tsx b/src/client/views/nodes/IconTagBox.tsx index 8faf8ffa5..ddabd61e1 100644 --- a/src/client/views/nodes/IconTagBox.tsx +++ b/src/client/views/nodes/IconTagBox.tsx @@ -67,7 +67,7 @@ export class IconTagBox extends ObservableReactComponent<IconTagProps> { render() { const buttons = Doc.MyFilterHotKeys .map(key => ({ key, tag: StrCast(key.toolType) })) - .filter(({ tag }) => this._props.IsEditing || TagItem.docHasTag(this.View.Document, tag) || (DocumentView.Selected.length === 1 && this.View.IsSelected)) + .filter(({ tag }) => this._props.IsEditing || TagItem.docHasTag(this.View.Document, tag) || (DocumentView.Selected().length === 1 && this.View.IsSelected)) .map(({ key, tag }) => ( <Tooltip key={tag} title={<div className="dash-tooltip">Click to add/remove this card from the {tag} group</div>}> <button diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index fd1c791d3..6289470b6 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -149,7 +149,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<FieldViewProps>() componentDidMount() { this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = 0; - this._props.setContentViewBox?.(this); // this tells the DocumentView that this Box is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. + this._props.setContentViewBox?.(this); // this tells the DocumentView that this ScreenshotBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. // this.layoutDoc.videoWall && reaction(() => ({ width: this._props.PanelWidth(), height: this._props.PanelHeight() }), // ({ width, height }) => { // if (this._camera) { diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index 123d32301..aa3d73e3e 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -103,6 +103,26 @@ export class InkField extends ObjectField { const top = Math.min(...ys); return { right, left, bottom, top, width: right - left, height: bottom - top }; } + + // 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 + public static 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); + } } ScriptingGlobals.add('InkField', InkField); diff --git a/src/pen-gestures/GestureTypes.ts b/src/pen-gestures/GestureTypes.ts index 2e1c9d16f..5a8e9bd97 100644 --- a/src/pen-gestures/GestureTypes.ts +++ b/src/pen-gestures/GestureTypes.ts @@ -1,7 +1,6 @@ export enum Gestures { Line = 'line', Stroke = 'stroke', - Scribble = 'scribble', Text = 'text', Triangle = 'triangle', Circle = 'circle', diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts index 31a4eb0e8..04262b61f 100644 --- a/src/pen-gestures/ndollar.ts +++ b/src/pen-gestures/ndollar.ts @@ -263,75 +263,7 @@ export class NDollarRecognizer { ]) ) ); - this.Multistrokes.push( - new Multistroke( - Gestures.Scribble, - useBoundedRotationInvariance, - new Array([ - new Point(232.43359374999994, 428.3789062499997), - new Point(232.43359374999994, 382.0359155790783), - new Point(265.26320387389393, 340.59025405258205), - new Point(285.92968749999994, 300.6953124999997), - new Point(285.92968749999994, 300.6953124999997), - new Point(290.29904726504463, 292.260623967478), - new Point(294.85514947688137, 276.5586112000384), - new Point(301.80468749999994, 270.1054687499997), - new Point(301.80468749999994, 270.1054687499997), - new Point(303.30831790356257, 268.7092405181201), - new Point(302.3387333271824, 274.174445118092), - new Point(302.58984374999994, 276.2109374999997), - new Point(302.58984374999994, 276.2109374999997), - new Point(303.5898583815426, 284.3210038050238), - new Point(306.34100306922323, 291.42526150092345), - new Point(308.55078124999994, 299.2578124999997), - new Point(308.55078124999994, 299.2578124999997), - new Point(314.02357444430737, 318.65610875195364), - new Point(312.83820955386796, 338.9193216781303), - new Point(317.41015624999994, 358.4374999999997), - new Point(317.41015624999994, 358.4374999999997), - new Point(324.6448511447705, 389.323263646351), - new Point(341.2272901550544, 419.08257022687917), - new Point(351.55468749999994, 449.3085937499997), - new Point(351.55468749999994, 449.3085937499997), - new Point(354.4485190828321, 457.7782031368645), - new Point(359.85272292551673, 488.59621490807643), - new Point(368.55859374999994, 492.7578124999997), - new Point(368.55859374999994, 492.7578124999997), - new Point(368.9336613544369, 492.9371030581646), - new Point(375.30018285197116, 475.54269522741924), - new Point(385.01171874999994, 460.68749999999966), - new Point(385.01171874999994, 460.68749999999966), - new Point(409.01141338031505, 423.9765046563952), - new Point(465.73402430232653, 338.122252279478), - new Point(470.49609374999994, 336.8945312499997), - new Point(470.49609374999994, 336.8945312499997), - new Point(472.34967396580504, 336.41665510061245), - new Point(470.5375229924171, 340.7226912215484), - new Point(470.56249999999994, 342.6367187499997), - new Point(470.56249999999994, 342.6367187499997), - new Point(470.6277554579174, 347.63734752514455), - new Point(471.4666087447205, 352.597933700578), - new Point(472.79687499999994, 357.4218749999997), - new Point(472.79687499999994, 357.4218749999997), - new Point(478.7003095537336, 378.82948542322754), - new Point(492.1754046938961, 397.52358353298115), - new Point(497.80468749999994, 418.6796874999997), - new Point(497.80468749999994, 418.6796874999997), - new Point(499.2975220287686, 424.29009358262533), - new Point(498.6576561843205, 452.5736061983319), - new Point(502.21874999999994, 455.3749999999997), - new Point(502.21874999999994, 455.3749999999997), - new Point(502.3599404176782, 455.4860697952399), - new Point(526.9859878583989, 412.7902963819643), - new Point(528.28515625, 410.5507812499997), - new Point(528.28515625, 410.5507812499997), - new Point(534.3674131478522, 400.0661462732759), - new Point(586.26953125, 311.2520380124085), - new Point(586.26953125, 306.8515624999997), - ]) - ) - ); - this.Multistrokes.push(new Multistroke(Gestures.RightAngle, useBoundedRotationInvariance, new Array([new Point(200, 0), new Point(0, 0), new Point(0, 100)]))); + this.Multistrokes.push(new Multistroke(Gestures.RightAngle, useBoundedRotationInvariance, new Array([new Point(0, 0), new Point(0, 100), new Point(200, 100)]))); NumMultistrokes = this.Multistrokes.length; // NumMultistrokes flags the end of the non user-defined gstures strokes // // PREDEFINED STROKES |