diff options
Diffstat (limited to 'src')
34 files changed, 2489 insertions, 558 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex 75cff7b55..9130ba97c 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts index 05007960d..231688265 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -53,6 +53,12 @@ const callTypeMap: { [type: string]: GPTCallOpts } = { temp: 0, prompt: 'List unique differences between the content of the UserAnswer and Rubric. Before each difference, label it and provide any additional information the UserAnswer missed and explain it in second person without separating it into UserAnswer and Rubric content and additional information. If there are no differences, say correct', }, + draw: { + model: 'gpt-4o', + maxTokens: 1024, + temp: 0.5, + prompt: 'Given an item, a level of complexity from 1-10, and a size in pixels, generate a detailed and colored line drawing representation of it. Make sure every element has the stroke field filled out. More complex drawings will have much more detail and strokes. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, polygon, and path with M, Q, C, and L so only use those.', + }, }; let lastCall = ''; @@ -149,5 +155,35 @@ const gptImageLabel = async (src: string): Promise<string> => { return 'Error connecting with API'; } }; +const gptHandwriting = async (src: string): Promise<string> => { + try { + const response = await openai.chat.completions.create({ + model: 'gpt-4o', + temperature: 0, + messages: [ + { + role: 'user', + content: [ + { type: 'text', text: 'What is this does this handwriting say. Only return the text' }, + { + type: 'image_url', + image_url: { + url: `${src}`, + detail: 'low', + }, + }, + ], + }, + ], + }); + if (response.choices[0].message.content) { + return response.choices[0].message.content; + } + return 'Missing labels'; + } catch (err) { + console.log(err); + return 'Error connecting with API'; + } +}; -export { gptAPICall, gptImageCall, GPTCallType, gptImageLabel, gptGetEmbedding }; +export { gptAPICall, gptImageCall, GPTCallType, gptImageLabel, gptGetEmbedding, gptHandwriting }; diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts index 9808b6a01..9f46b8685 100644 --- a/src/client/cognitive_services/CognitiveServices.ts +++ b/src/client/cognitive_services/CognitiveServices.ts @@ -46,14 +46,15 @@ export enum Confidence { export namespace CognitiveServices { const ExecuteQuery = async <D>(service: Service, manager: APIManager<D>, data: D): Promise<any> => { const apiKey = process.env[service.toUpperCase()]; - if (!apiKey) { + if (apiKey) { + console.log(data) console.log(`No API key found for ${service}: ensure youe root directory has .env file with _CLIENT_${service.toUpperCase()}.`); return undefined; } let results: any; try { - results = await manager.requester(apiKey, manager.converter(data), service).then(json => JSON.parse(json)); + results = await manager.requester("has", manager.converter(data), service).then(json => JSON.parse(json)); } catch (e) { throw e; } @@ -137,6 +138,12 @@ export namespace CognitiveServices { points: points.map(({ X: x, Y: y }) => `${x},${y}`).join(','), language: 'en-US', })); + console.log(JSON.stringify({ + version: 1, + language: 'en-US', + unit: 'mm', + strokes, + })) return JSON.stringify({ version: 1, language: 'en-US', diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index 8f95068db..03ae2efb7 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -26,6 +26,7 @@ export enum DocumentType { FUNCPLOT = 'funcplot', // function plotter MAP = 'map', DATAVIZ = 'dataviz', + ANNOPALETTE = 'annopalette', LOADING = 'loading', SIMULATION = 'simulation', // physics simulation diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index b96fdb4bd..d311ad971 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -767,7 +767,7 @@ export namespace Docs { export function ComparisonDocument(text: string, options: DocumentOptions = { title: 'Comparison Box' }) { return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), text, options); } - export function DiagramDocument(options: DocumentOptions = { title: 'bruh box' }) { + export function DiagramDocument(options: DocumentOptions = { title: '' }) { return InstanceFromProto(Prototypes.get(DocumentType.DIAGRAM), undefined, options); } @@ -1009,6 +1009,10 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.DATAVIZ), new CsvField(url), { title: 'Data Viz', type: 'dataviz', ...options }, undefined, undefined, undefined, overwriteDoc); } + export function AnnoPaletteDocument(options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.ANNOPALETTE), new List([Doc.MyAnnos]), { ...(options || {}) }); + } + export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) { const ret = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { treeView_FreezeChildren: 'remove|add', ...options, type_collection: CollectionViewType.Docking, dockingConfig: config }, id); documents.map(c => Doc.SetContainer(c, ret)); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index e095bc659..6b4f80e23 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -162,6 +162,26 @@ export class CurrentUserUtils { const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts); } + + static setupAnnoPalette(doc: Doc, field="myAnnos") { + const reqdOpts:DocumentOptions = { + title: "Saved Annotations", _xMargin: 0, _layout_showTitle: "title", hidden: false, + _dragOnlyWithinContainer: true, layout_hideContextMenu: true, isSystem: true, _forceActive: true, + _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, + }; + const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; + const savedAnnos = DocCast(doc[field]); + return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.MasonryDocument(items??[], opts), reqdOpts, DocListCast(savedAnnos?.data), reqdScripts); + } + + static setupLightboxDrawingPreviews(doc: Doc, field="myLightboxDrawings") { + const reqdOpts:DocumentOptions = { + title: "Preview", _header_height: 0, _layout_fitWidth: true, childLayoutFitWidth: true, + }; + const reqdScripts = {}; + const drawings = DocCast(doc[field]); + return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.CarouselDocument(items??[], opts), reqdOpts, DocListCast(drawings?.data), reqdScripts); + } // setup templates for different document types when they are iconified from Document Decorations static setupDefaultIconTemplates(doc: Doc, field="template_icons") { @@ -372,6 +392,7 @@ pie title Minerals in my tap water {key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }}, {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts), opts: { _width: 300, _height: 300 }}, {key: "Chat", creator: Docs.Create.ChatDocument, opts: { _width: 300, _height: 300, }}, + {key: "AnnoPalette",creator: Docs.Create.AnnoPaletteDocument, opts: {_width: 300, _height: 300, _dropAction: dropActionType.move }}, {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 120, _header_pointerEvents: "all", _header_height: 50, _header_fontSize: 9,_layout_autoHeightMargins: 50, _layout_autoHeight: true, treeView_HideUnrendered: true}}, {key: "ViewSlide", creator: slideView, opts: { _width: 400, _height: 300, _xMargin: 3, _yMargin: 3,}}, {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, dropAction: dropActionType.embed, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }}, @@ -398,7 +419,7 @@ pie title Minerals in my tap water { toolTip: "Tap or drag to create a plotly node", title: "Plotly", icon: "rocket", dragFactory: doc.emptyPlotly as Doc, clickFactory: DocCast(doc.emptyMermaids)}, { toolTip: "Tap or drag to create a physics simulation",title: "Simulation", icon: "rocket",dragFactory: doc.emptySimulation as Doc, clickFactory: DocCast(doc.emptySimulation), funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "book", dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)}, - { toolTip: "Tap or drag to create an iamge", title: "Image", icon: "image", dragFactory: doc.emptyImage as Doc, clickFactory: DocCast(doc.emptyImage)}, + { toolTip: "Tap or drag to create an image", title: "Image", icon: "image", dragFactory: doc.emptyImage as Doc, clickFactory: DocCast(doc.emptyImage)}, { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab)}, { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, clickFactory: DocCast(doc.emptyWebpage)}, { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, clickFactory: DocCast(doc.emptyComparison)}, @@ -414,6 +435,7 @@ pie title Minerals in my tap water { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "person-chalkboard", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a view slide", title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc,clickFactory: DocCast(doc.emptyViewSlide),openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}}, { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true, funcs: { hidden: "IsNoviceMode()"} }, + { toolTip: "Tap or drag to create an annotation palette",title: "Annotation Palette", icon: "palette", dragFactory: doc.emptyAnnoPalette as Doc, clickFactory: DocCast(doc.emptyAnnoPalette)}, { toolTip: "Toggle a Calculator REPL", title: "replviewer", icon: "calculator", clickFactory: '<ScriptingRepl />' as any, openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script // { toolTip: "Toggle an UndoStack", title: "undostacker", icon: "calculator", clickFactory: "<UndoStack />" as any, openFactoryLocation: OpenWhere.overlay}, ].map(tuple => ( @@ -984,6 +1006,8 @@ pie title Minerals in my tap water this.setupTopbarButtons(doc); this.setupDockedButtons(doc); // the bottom bar of font icons this.setupLeftSidebarMenu(doc); // the left-side column of buttons that open their contents in a flyout panel on the left + this.setupAnnoPalette(doc); + this.setupLightboxDrawingPreviews(doc); this.setupDocTemplates(doc); // sets up the template menu of templates // sthis.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption DocUtils.AssignDocField(doc, "globalScriptDatabase", () => Docs.Prototypes.MainScriptDocument(), {}); diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index 0314af06b..3e26fefad 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -86,6 +86,28 @@ export function makeUserTemplateButton(doc: Doc) { dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)'); return dbox; } + +export function makeUserTemplateImage(doc: Doc, image: ImageField) { + const layoutDoc = doc; // doc.layout instanceof Doc && doc.layout.isTemplateForField ? doc.layout : doc; + if (layoutDoc.type !== DocumentType.FONTICON) { + !layoutDoc.isTemplateDoc && makeTemplate(layoutDoc); + } + layoutDoc.isTemplateDoc = true; + const dbox = Docs.Create.ImageDocument(image, { + _nativeWidth: 100, + _nativeHeight: 100, + _width: 100, + _height: 100, + title: StrCast(layoutDoc.title), + isSystem: false, + }); + dbox.title = ComputedField.MakeFunction('this.dragFactory.title'); + dbox.dragFactory = layoutDoc; + dbox.dropPropertiesToRemove = doc.dropPropertiesToRemove instanceof ObjectField ? ObjectField.MakeCopy(doc.dropPropertiesToRemove) : undefined; + dbox.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory)'); + return dbox; +} + export function convertDropDataToButtons(data: DragManager.DocumentDragData) { data?.draggedDocuments.forEach((doc, i) => { let dbox = doc; diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts index d6f3f2340..e2055887d 100644 --- a/src/client/util/bezierFit.ts +++ b/src/client/util/bezierFit.ts @@ -586,6 +586,146 @@ export function FitOneCurve(d: Point[], tHat1?: Point, tHat2?: Point) { return { finalCtrls, error }; } +// alpha determines how far away the tangents are, or the "tightness" of the bezier +export function GenerateControlPoints(coordinates: Point[], alpha = 0.1) { + const firstEnd = coordinates.length ? [coordinates[0], coordinates[0]] : []; + const lastEnd = coordinates.length ? [coordinates.lastElement(), coordinates.lastElement()] : []; + const points: Point[] = coordinates.slice(1, coordinates.length - 1).flatMap((pt, index, inkData) => { + const prevPt: Point = index === 0 ? firstEnd[0] : inkData[index - 1]; + const nextPt: Point = index === inkData.length - 1 ? lastEnd[0] : inkData[index + 1]; + if (prevPt.X === nextPt.X) { + const verticalDist = nextPt.Y - prevPt.Y; + return [{ X: pt.X, Y: pt.Y - alpha * verticalDist }, pt, pt, { X: pt.X, Y: pt.Y + alpha * verticalDist }]; + } else if (prevPt.Y === nextPt.Y) { + const horizDist = nextPt.X - prevPt.X; + return [{ X: pt.X - alpha * horizDist, Y: pt.Y }, pt, pt, { X: pt.X + alpha * horizDist, Y: pt.Y }]; + } + // tangent vectors between the adjacent points + const tanX = nextPt.X - prevPt.X; + const tanY = nextPt.Y - prevPt.Y; + const ctrlPt1: Point = { X: pt.X - alpha * tanX, Y: pt.Y - alpha * tanY }; + const ctrlPt2: Point = { X: pt.X + alpha * tanX, Y: pt.Y + alpha * tanY }; + return [ctrlPt1, pt, pt, ctrlPt2]; + }); + return [...firstEnd, ...points, ...lastEnd]; +} + +export function SVGToBezier(name: SVGType, attributes: any): Point[] { + switch (name) { + case 'line': + const x1 = parseInt(attributes.x1); + const x2 = parseInt(attributes.x2); + const y1 = parseInt(attributes.y1); + const y2 = parseInt(attributes.y2); + return [ + { X: x1, Y: y1 }, + { X: x1, Y: y1 }, + { X: x2, Y: y2 }, + { X: x2, Y: y2 }, + ]; + case 'circle': + case 'ellipse': + const c = 0.551915024494; + const centerX = parseInt(attributes.cx); + const centerY = parseInt(attributes.cy); + const radiusX = parseInt(attributes.rx) || parseInt(attributes.r); + const radiusY = parseInt(attributes.ry) || parseInt(attributes.r); + return [ + { X: centerX, Y: centerY + radiusY }, + { X: centerX + c * radiusX, Y: centerY + radiusY }, + { X: centerX + radiusX, Y: centerY + c * radiusY }, + { X: centerX + radiusX, Y: centerY }, + { X: centerX + radiusX, Y: centerY }, + { X: centerX + radiusX, Y: centerY - c * radiusY }, + { X: centerX + c * radiusX, Y: centerY - radiusY }, + { X: centerX, Y: centerY - radiusY }, + { X: centerX, Y: centerY - radiusY }, + { X: centerX - c * radiusX, Y: centerY - radiusY }, + { X: centerX - radiusX, Y: centerY - c * radiusY }, + { X: centerX - radiusX, Y: centerY }, + { X: centerX - radiusX, Y: centerY }, + { X: centerX - radiusX, Y: centerY + c * radiusY }, + { X: centerX - c * radiusX, Y: centerY + radiusY }, + { X: centerX, Y: centerY + radiusY }, + ]; + case 'rect': + const x = parseInt(attributes.x); + const y = parseInt(attributes.y); + const width = parseInt(attributes.width); + const height = parseInt(attributes.height); + return [ + { X: x, Y: y }, + { X: x, Y: y }, + { X: x + width, Y: y }, + { X: x + width, Y: y }, + { X: x + width, Y: y }, + { X: x + width, Y: y }, + { X: x + width, Y: y + height }, + { X: x + width, Y: y + height }, + { X: x + width, Y: y + height }, + { X: x + width, Y: y + height }, + { X: x, Y: y + height }, + { X: x, Y: y + height }, + { X: x, Y: y + height }, + { X: x, Y: y + height }, + { X: x, Y: y }, + { X: x, Y: y }, + ]; + case 'path': + const coordList: Point[] = []; + const startPt = attributes.d.match(/M(-?\d+\.?\d*),(-?\d+\.?\d*)/); + coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) }); + const matches: RegExpMatchArray[] = Array.from( + attributes.d.matchAll(/Q(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|C(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|L(-?\d+\.?\d*),(-?\d+\.?\d*)/g) + ); + let lastPt: Point; + matches.forEach(match => { + if (match[0].startsWith('Q')) { + coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) }); + coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) }); + coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) }); + coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) }); + lastPt = { X: parseInt(match[3]), Y: parseInt(match[4]) }; + } else if (match[0].startsWith('C')) { + coordList.push({ X: parseInt(match[5]), Y: parseInt(match[6]) }); + coordList.push({ X: parseInt(match[7]), Y: parseInt(match[8]) }); + coordList.push({ X: parseInt(match[9]), Y: parseInt(match[10]) }); + coordList.push({ X: parseInt(match[9]), Y: parseInt(match[10]) }); + lastPt = { X: parseInt(match[9]), Y: parseInt(match[10]) }; + } else { + coordList.push(lastPt || { X: parseInt(startPt[1]), Y: parseInt(startPt[2]) }); + coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) }); + coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) }); + coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) }); + lastPt = { X: parseInt(match[11]), Y: parseInt(match[12]) }; + } + }); + const hasZ = attributes.d.match(/Z/); + if (hasZ) { + coordList.push(lastPt); + coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) }); + coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) }); + } else { + coordList.pop(); + } + return coordList; + case 'polygon': + const coords: RegExpMatchArray[] = Array.from(attributes.points.matchAll(/(-?\d+\.?\d*),(-?\d+\.?\d*)/g)); + let list: Point[] = []; + coords.forEach(coord => { + list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) }); + list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) }); + list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) }); + list.push({ X: parseInt(coord[1]), Y: parseInt(coord[2]) }); + }); + const firstPts = list.splice(0, 2); + list = list.concat(firstPts); + return list; + default: + return []; + } +} + /* static double GetTValueFromSValue (const BezierRep &parent, double t, double endT, bool left, double influenceDistance, double &excess) { double dist = 0; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 487868169..15e90ac2a 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -12,7 +12,7 @@ import * as React from 'react'; import { FaEdit } from 'react-icons/fa'; import { returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../ClientUtils'; import { emptyFunction } from '../../Utils'; -import { Doc } from '../../fields/Doc'; +import { Doc, DocListCast } from '../../fields/Doc'; import { Cast, DocCast } from '../../fields/Types'; import { DocUtils, IsFollowLinkScript } from '../documents/DocUtils'; import { CalendarManager } from '../util/CalendarManager'; @@ -31,6 +31,8 @@ import { DocumentLinksButton } from './nodes/DocumentLinksButton'; import { DocumentView } from './nodes/DocumentView'; import { OpenWhere } from './nodes/OpenWhere'; import { DashFieldView } from './nodes/formattedText/DashFieldView'; +import { AnnotationPalette } from './smartdraw/AnnotationPalette'; +import { DocData } from '../../fields/DocSymbols'; @observer export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (DocumentView | undefined)[]; stack?: any }> { @@ -241,6 +243,23 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( ); } + @undoBatch + saveAnno = action(async (targetDoc: Doc) => { + await AnnotationPalette.addToPalette(targetDoc); + }); + + @computed + get saveAnnoButton() { + const targetDoc = this.view0?.Document; + return !targetDoc ? null : ( + <Tooltip title={<div className="dash-tooltip">{targetDoc.savedAsAnno ? 'Saved as Annotation!' : 'Save to Annotation Palette'}</div>}> + <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={() => this.saveAnno(targetDoc)}> + <FontAwesomeIcon className="documentdecorations-icon" icon={targetDoc.savedAsAnno ? 'clipboard-check' : 'file-arrow-down'} /> + </div> + </Tooltip> + ); + } + @computed get shareButton() { const targetDoc = this.view0?.Document; @@ -450,6 +469,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => ( <div className="documentButtonBar-button">{this.templateButton}</div> {!DocumentView.Selected().some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>} <div className="documentButtonBar-button">{this.pinButton}</div> + <div className="documentButtonBar-button">{this.saveAnnoButton}</div> <div className="documentButtonBar-button">{this.recordButton}</div> <div className="documentButtonBar-button">{this.calendarButton}</div> {!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>} diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 93c3e3338..ae4f5b98b 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -229,6 +229,11 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora if (iconViewDoc.activeFrame) { iconViewDoc.opacity = 0; // bcz: hacky ... allows inkMasks and other documents to be "turned off" without removing them from the animated collection which allows them to function properly in a presenation. } else { + // to mark annotations as no longer saved if they're deleted from the palette + const dragFactory: Doc = DocCast(iconView.Document.dragFactory); + if (dragFactory && DocCast(dragFactory.cloneOf).savedAsAnno) { + DocCast(dragFactory.cloneOf).savedAsAnno = undefined; + } iconView._props.removeDocument?.(iconView.Document); } }); diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index e3e252593..e961bc031 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -94,6 +94,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil @action onPointerDown = (e: React.PointerEvent) => { + console.log('pointerdown'); if (!(e.target as any)?.className?.toString().startsWith('lm_')) { if ([InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { this._points.push({ X: e.clientX, Y: e.clientY }); @@ -132,6 +133,7 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil } @action onPointerUp = () => { + console.log('pointer up'); DocumentView.DownDocView = undefined; if (this._points.length > 1) { const B = this.svgBounds; @@ -147,7 +149,9 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil else { // need to decide when to turn gestures back on const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize([points]); + console.log(points); let actionPerformed = false; + console.log(result); if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) { switch (result.Name) { case Gestures.Line: @@ -156,10 +160,14 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil case Gestures.Circle: this.makeBezierPolygon(result.Name, true); actionPerformed = this.dispatchGesture(result.Name); + console.log(result.Name); + console.log(); break; case Gestures.Scribble: console.log('scribble'); break; + case Gestures.RightAngle: + console.log('RightAngle'); default: } } diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 7d01bbabb..562827db5 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -162,7 +162,7 @@ export class KeyManager { case 'delete': case 'backspace': if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') { - if (DocumentView.LightboxDoc()) { + if (DocumentView.LightboxDoc() && !DocumentView.Selected().length) { DocumentView.SetLightboxDoc(undefined); DocumentView.DeselectAll(); } else if (!window.getSelection()?.toString()) DocumentDecorations.Instance.onCloseClick(true); diff --git a/src/client/views/InkTranscription.scss b/src/client/views/InkTranscription.scss index bbb0a1afa..18d6b8b10 100644 --- a/src/client/views/InkTranscription.scss +++ b/src/client/views/InkTranscription.scss @@ -2,4 +2,9 @@ .error-msg { display: none !important; } + .ms-editor{ + .smartguide{ + top:1000px; + } + } }
\ No newline at end of file diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx index 1ed8de1be..3f90df7d1 100644 --- a/src/client/views/InkTranscription.tsx +++ b/src/client/views/InkTranscription.tsx @@ -1,349 +1,416 @@ -// import * as iink from 'iink-js'; -// 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, NumCast } from '../../fields/Types'; -// import { aggregateBounds } from '../../Utils'; -// import { DocumentType } from '../documents/DocumentTypes'; -// import { CollectionFreeFormView } from './collections/collectionFreeForm'; -// import { InkingStroke } from './InkingStroke'; -// import './InkTranscription.scss'; - -// /** -// * Class component that handles inking in writing mode -// */ -// export class InkTranscription extends React.Component { -// static Instance: InkTranscription; - -// @observable _mathRegister: any= undefined; -// @observable _mathRef: any= undefined; -// @observable _textRegister: any= undefined; -// @observable _textRef: any= undefined; -// private lastJiix: any; -// private currGroup?: Doc; - -// constructor(props: Readonly<{}>) { -// 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 -// setMathRef = (r: any) => { -// if (!this._mathRegister) { -// this._mathRegister = r -// ? iink.register(r, { -// recognitionParams: { -// type: 'MATH', -// protocol: 'WEBSOCKET', -// server: { -// host: 'cloud.myscript.com', -// applicationKey: process.env.IINKJS_APP, -// hmacKey: process.env.IINKJS_HMAC, -// websocket: { -// pingEnabled: false, -// autoReconnect: true, -// }, -// }, -// iink: { -// math: { -// mimeTypes: ['application/x-latex', 'application/vnd.myscript.jiix'], -// }, -// export: { -// jiix: { -// strokes: true, -// }, -// }, -// }, -// }, -// }) -// : null; -// } - -// r?.addEventListener('exported', (e: any) => this.exportInk(e, this._mathRef)); - -// return (this._mathRef = r); -// }; - -// @action -// setTextRef = (r: any) => { -// if (!this._textRegister) { -// this._textRegister = r -// ? iink.register(r, { -// recognitionParams: { -// type: 'TEXT', -// protocol: 'WEBSOCKET', -// server: { -// host: 'cloud.myscript.com', -// applicationKey: '7277ec34-0c2e-4ee1-9757-ccb657e3f89f', -// hmacKey: 'f5cb18f2-1f95-4ddb-96ac-3f7c888dffc1', -// websocket: { -// pingEnabled: false, -// autoReconnect: true, -// }, -// }, -// iink: { -// text: { -// mimeTypes: ['text/plain'], -// }, -// export: { -// jiix: { -// strokes: true, -// }, -// }, -// }, -// }, -// }) -// : null; -// } - -// r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef)); - -// return (this._textRef = r); -// }; - -// /** -// * Handles processing Dash Doc data for ink transcription. -// * -// * @param groupDoc the group which contains the ink strokes we want to transcribe -// * @param inkDocs the ink docs contained within the selected group -// * @param math boolean whether to do math transcription or not -// */ -// transcribeInk = (groupDoc: Doc | undefined, inkDocs: Doc[], math: boolean) => { -// if (!groupDoc) return; -// const validInks = inkDocs.filter(s => s.type === DocumentType.INK); - -// const strokes: InkData[] = []; -// const times: number[] = []; -// validInks -// .filter(i => Cast(i[Doc.LayoutFieldKey(i)], InkField)) -// .forEach(i => { -// const d = Cast(i[Doc.LayoutFieldKey(i)], InkField, null); -// const inkStroke = DocumentManager.Instance.getDocumentView(i)?.ComponentView as InkingStroke; -// strokes.push(d.inkData.map(pd => inkStroke.ptToScreen({ X: pd.X, Y: pd.Y }))); -// times.push(DateCast(i.author_date).getDate().getTime()); -// }); - -// this.currGroup = groupDoc; - -// const pointerData = { events: strokes.map((stroke, i) => this.inkJSON(stroke, times[i])) }; -// const processGestures = false; - -// if (math) { -// this._mathRef.editor.pointerEvents(pointerData, processGestures); -// } else { -// this._textRef.editor.pointerEvents(pointerData, processGestures); -// } -// }; - -// /** -// * Converts the Dash Ink Data to JSON. -// * -// * @param stroke The dash ink data -// * @param time the time of the stroke -// * @returns json object representation of ink data -// */ -// inkJSON = (stroke: InkData, time: number) => { -// return { -// pointerType: 'PEN', -// pointerId: 1, -// x: stroke.map(point => point.X), -// y: stroke.map(point => point.Y), -// t: new Array(stroke.length).fill(time), -// p: new Array(stroke.length).fill(1.0), -// }; -// }; - -// /** -// * Creates subgroups for each word for the whole text transcription -// * @param wordInkDocMap the mapping of words to ink strokes (Ink Docs) -// */ -// subgroupsTranscriptions = (wordInkDocMap: Map<string, Doc[]>) => { -// // iterate through the keys of wordInkDocMap -// wordInkDocMap.forEach(async (inkDocs: Doc[], word: string) => { -// const selected = inkDocs.slice(); -// if (!selected) { -// return; -// } -// const ctx = await Cast(selected[0].embedContainer, Doc); -// if (!ctx) { -// return; -// } -// const docView: CollectionFreeFormView = DocumentManager.Instance.getDocumentView(ctx)?.ComponentView as CollectionFreeFormView; - -// if (!docView) return; -// const marqViewRef = docView._marqueeViewRef.current; -// if (!marqViewRef) return; -// this.groupInkDocs(selected, docView, word); -// }); -// }; - -// /** -// * Event listener function for when the 'exported' event is heard. -// * -// * @param e the event objects -// * @param ref the ref to the editor -// */ -// exportInk = (e: any, ref: any) => { -// const exports = e.detail.exports; -// if (exports) { -// if (exports['application/x-latex']) { -// const latex = exports['application/x-latex']; -// if (this.currGroup) { -// this.currGroup.text = latex; -// this.currGroup.title = latex; -// } - -// ref.editor.clear(); -// } else if (exports['text/plain']) { -// if (exports['application/vnd.myscript.jiix']) { -// this.lastJiix = JSON.parse(exports['application/vnd.myscript.jiix']); -// // map timestamp to strokes -// const timestampWord = new Map<number, string>(); -// 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> }) => { -// const ms = Date.parse(i.timestamp); -// timestampWord.set(ms, word.label); -// }); -// } -// }); - -// const wordInkDocMap = new Map<string, Doc[]>(); -// if (this.currGroup) { -// const docList = DocListCast(this.currGroup.data); -// docList.forEach((inkDoc: Doc) => { -// // just having the times match up and be a unique value (actual timestamp doesn't matter) -// const ms = DateCast(inkDoc.author_date).getDate().getTime() + 14400000; -// const word = timestampWord.get(ms); -// if (!word) { -// return; -// } -// const entry = wordInkDocMap.get(word); -// if (entry) { -// entry.push(inkDoc); -// wordInkDocMap.set(word, entry); -// } else { -// const newEntry = [inkDoc]; -// wordInkDocMap.set(word, newEntry); -// } -// }); -// if (this.lastJiix.words.length > 1) this.subgroupsTranscriptions(wordInkDocMap); -// } -// } -// const text = exports['text/plain']; - -// if (this.currGroup) { -// this.currGroup.transcription = text; -// this.currGroup.title = text.split('\n')[0]; -// } - -// ref.editor.clear(); -// } -// } -// }; - -// /** -// * Creates the ink grouping once the user leaves the writing mode. -// */ -// createInkGroup() { -// // TODO nda - if document being added to is a inkGrouping then we can just add to that group -// if (Doc.ActiveTool === InkTool.Write) { -// CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => { -// // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those -// const selected = ffView.unprocessedDocs; -// const newCollection = this.groupInkDocs( -// selected.filter(doc => doc.embedContainer), -// ffView -// ); -// ffView.unprocessedDocs = []; - -// InkTranscription.Instance.transcribeInk(newCollection, selected, false); -// }); -// } -// CollectionFreeFormView.collectionsWithUnprocessedInk.clear(); -// } - -// /** -// * Creates the groupings for a given list of ink docs on a specific doc view -// * @param selected: the list of ink docs to create a grouping of -// * @param docView: the view in which we want the grouping to be created -// * @param word: optional param if the group we are creating is a word (subgrouping individual words) -// * @returns a new collection Doc or undefined if the grouping fails -// */ -// groupInkDocs(selected: Doc[], docView: CollectionFreeFormView, word?: string): Doc | undefined { -// const bounds: { x: number; y: number; width?: number; height?: number }[] = []; - -// // calculate the necessary bounds from the selected ink docs -// selected.map( -// action(d => { -// const x = NumCast(d.x); -// const y = NumCast(d.y); -// const width = NumCast(d._width); -// const height = NumCast(d._height); -// bounds.push({ x, y, width, height }); -// }) -// ); - -// // calculate the aggregated bounds -// const aggregBounds = aggregateBounds(bounds, 0, 0); -// const marqViewRef = docView._marqueeViewRef.current; - -// // set the vals for bounds in marqueeView -// if (marqViewRef) { -// marqViewRef._downX = aggregBounds.x; -// marqViewRef._downY = aggregBounds.y; -// marqViewRef._lastX = aggregBounds.r; -// marqViewRef._lastY = aggregBounds.b; -// } - -// // map through all the selected ink strokes and create the groupings -// selected.map( -// action(d => { -// const dx = NumCast(d.x); -// const dy = NumCast(d.y); -// delete d.x; -// delete d.y; -// delete d.activeFrame; -// delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection -// delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection -// // calculate pos based on bounds -// if (marqViewRef?.Bounds) { -// d.x = dx - marqViewRef.Bounds.left - marqViewRef.Bounds.width / 2; -// d.y = dy - marqViewRef.Bounds.top - marqViewRef.Bounds.height / 2; -// } -// return d; -// }) -// ); -// docView.props.removeDocument?.(selected); -// // Gets a collection based on the selected nodes using a marquee view ref -// const newCollection = marqViewRef?.getCollection(selected, undefined, true); -// if (newCollection) { -// newCollection.width = NumCast(newCollection._width); -// newCollection.height = NumCast(newCollection._height); -// // if the grouping we are creating is an individual word -// if (word) { -// newCollection.title = word; -// } -// } - -// // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs -// newCollection && docView.props.addDocument?.(newCollection); -// return newCollection; -// } - -// 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> -// ); -// } -// } +import * as iink from 'iink-ts'; +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, ImageCast, NumCast, StrCast } from '../../fields/Types'; +import { aggregateBounds } from '../../Utils'; +import { DocumentType } from '../documents/DocumentTypes'; +import { CollectionFreeFormView } from './collections/collectionFreeForm'; +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 + */ +export class InkTranscription extends React.Component { + static Instance: InkTranscription; + + @observable _mathRegister: any = undefined; + @observable _mathRef: any = undefined; + @observable _textRegister: any = undefined; + @observable _textRef: any = undefined; + @observable iinkEditor: any = undefined; + private lastJiix: any; + private currGroup?: Doc; + private collectionFreeForm?: CollectionFreeFormView; + + constructor(props: Readonly<{}>) { + 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 + setMathRef = async (r: any) => { + if (!this._textRegister && r) { + let editor; + const options = { + configuration: { + server: { + scheme: 'https', + host: 'cloud.myscript.com', + applicationKey: 'c0901093-5ac5-4454-8e64-0def0f13f2ca', + hmacKey: 'f6465cca-1856-4492-a6a4-e2395841be2f', + protocol: 'WEBSOCKET', + }, + recognition: { + type: 'TEXT', + }, + }, + }; + + editor = new iink.Editor(r, options as any); + + await editor.initialize(); + + this._textRegister = r; + r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef)); + + return (this._textRef = r); + } + }; + @action + setTextRef = async (r: any) => { + if (!this._textRegister && r) { + let editor; + const options = { + configuration: { + server: { + scheme: 'https', + host: 'cloud.myscript.com', + applicationKey: 'c0901093-5ac5-4454-8e64-0def0f13f2ca', + hmacKey: 'f6465cca-1856-4492-a6a4-e2395841be2f', + protocol: 'WEBSOCKET', + }, + recognition: { + type: 'TEXT', + }, + }, + }; + + editor = new iink.Editor(r, options as any); + + await editor.initialize(); + this.iinkEditor = editor; + this._textRegister = r; + r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef)); + + return (this._textRef = r); + } + }; + + /** + * Handles processing Dash Doc data for ink transcription. + * + * @param groupDoc the group which contains the ink strokes we want to transcribe + * @param inkDocs the ink docs contained within the selected group + * @param math boolean whether to do math transcription or not + */ + transcribeInk = (groupDoc: Doc | undefined, inkDocs: Doc[], math: boolean) => { + if (!groupDoc) return; + const validInks = inkDocs.filter(s => s.type === DocumentType.INK); + + const strokes: InkData[] = []; + const times: number[] = []; + validInks + .filter(i => Cast(i[Doc.LayoutFieldKey(i)], InkField)) + .forEach(i => { + const d = Cast(i[Doc.LayoutFieldKey(i)], InkField, null); + const inkStroke = DocumentView.getDocumentView(i)?.ComponentView as InkingStroke; + 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.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); + } + }; + convertPointsToString(points: InkData[]): string { + return points[0].map(point => `new Point(${point.X}, ${point.Y})`).join(',\n '); + } + convertPointsToString2(points: InkData[]): string { + return points[0].map(point => `(${point.X},${point.Y})`).join(','); + } + + /** + * Converts the Dash Ink Data to JSON. + * + * @param stroke The dash ink data + * @param time the time of the stroke + * @returns json object representation of ink data + */ + inkJSON = (stroke: InkData, time: number) => { + interface strokeData { + x: number; + y: number; + t: number; + p: number; + } + let strokeObjects: strokeData[] = []; + stroke.forEach(point => { + let tempObject: strokeData = { + x: point.X, + y: point.Y, + t: time, + p: 1.0, + }; + strokeObjects.push(tempObject); + }); + return { + pointerType: 'PEN', + pointerId: 1, + pointers: strokeObjects, + }; + }; + + /** + * Creates subgroups for each word for the whole text transcription + * @param wordInkDocMap the mapping of words to ink strokes (Ink Docs) + */ + subgroupsTranscriptions = (wordInkDocMap: Map<string, Doc[]>) => { + // iterate through the keys of wordInkDocMap + wordInkDocMap.forEach(async (inkDocs: Doc[], word: string) => { + const selected = inkDocs.slice(); + if (!selected) { + return; + } + const ctx = await Cast(selected[0].embedContainer, Doc); + if (!ctx) { + return; + } + const docView: CollectionFreeFormView = DocumentView.getDocumentView(ctx)?.ComponentView as CollectionFreeFormView; + // DocumentManager.Instance.getDocumentView(ctx)?.ComponentView as CollectionFreeFormView; + + if (!docView) return; + const marqViewRef = docView._marqueeViewRef.current; + if (!marqViewRef) return; + this.groupInkDocs(selected, docView, word); + }); + }; + + /** + * Event listener function for when the 'exported' event is heard. + * + * @param e the event objects + * @param ref the ref to the editor + */ + exportInk = async (e: any, ref: any) => { + const exports = e.detail['application/vnd.myscript.jiix']; + if (exports) { + if (exports['type'] == 'Math') { + const latex = exports['application/x-latex']; + if (this.currGroup) { + this.currGroup.text = latex; + this.currGroup.title = latex; + } + + ref.editor.clear(); + } else if (exports['type'] == 'Text') { + if (exports['application/vnd.myscript.jiix']) { + this.lastJiix = JSON.parse(exports['application/vnd.myscript.jiix']); + // map timestamp to strokes + const timestampWord = new Map<number, string>(); + 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> }) => { + const ms = Date.parse(i.timestamp); + timestampWord.set(ms, word.label); + }); + } + }); + + const wordInkDocMap = new Map<string, Doc[]>(); + if (this.currGroup) { + const docList = DocListCast(this.currGroup.data); + docList.forEach((inkDoc: Doc) => { + // just having the times match up and be a unique value (actual timestamp doesn't matter) + const ms = DateCast(inkDoc.author_date).getDate().getTime() + 14400000; + const word = timestampWord.get(ms); + if (!word) { + return; + } + const entry = wordInkDocMap.get(word); + if (entry) { + entry.push(inkDoc); + wordInkDocMap.set(word, entry); + } else { + const newEntry = [inkDoc]; + wordInkDocMap.set(word, newEntry); + } + }); + if (this.lastJiix.words.length > 1) this.subgroupsTranscriptions(wordInkDocMap); + } + } + const text = exports['label']; + + if (this.currGroup && text) { + DocumentView.getDocumentView(this.currGroup)?.ComponentView?.updateIcon?.(); + this.currGroup.transcription = text; + this.currGroup.title = text; + let image = await this.getIcon(); + const pathname = image?.url.href as string; + console.log(image?.url); + console.log(image); + const { href } = (image as URLField).url; + const hrefParts = href.split('.'); + const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`; + let response; + try { + const hrefBase64 = await this.imageUrlToBase64(hrefComplete); + response = await gptHandwriting(hrefBase64); + console.log(response); + } catch (error) { + console.log('bad things have happened'); + } + const textBoxText = 'iink: ' + text + '\n' + '\n' + 'ChatGPT: ' + 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; + this.collectionFreeForm?.addDocument(newDoc); + this.currGroup.hasTextBox = true; + } + ref.editor.clear(); + } + } + } + }; + async getIcon() { + const docView = DocumentView.getDocumentView(this.currGroup); + console.log(this.currGroup); + if (docView) { + console.log(docView); + docView.ComponentView?.updateIcon?.(); + return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000)); + } + return undefined; + } + imageUrlToBase64 = async (imageUrl: string): Promise<string> => { + try { + const response = await fetch(imageUrl); + const blob = await response.blob(); + + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.readAsDataURL(blob); + reader.onloadend = () => resolve(reader.result as string); + reader.onerror = error => reject(error); + }); + } catch (error) { + console.error('Error:', error); + throw error; + } + }; + + /** + * Creates the ink grouping once the user leaves the writing mode. + */ + createInkGroup() { + // TODO nda - if document being added to is a inkGrouping then we can just add to that group + if (Doc.ActiveTool === InkTool.Write) { + CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => { + // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those + const selected = ffView.unprocessedDocs; + const newCollection = this.groupInkDocs( + selected.filter(doc => doc.embedContainer), + ffView + ); + ffView.unprocessedDocs = []; + + InkTranscription.Instance.transcribeInk(newCollection, selected, false); + }); + } + CollectionFreeFormView.collectionsWithUnprocessedInk.clear(); + } + + /** + * Creates the groupings for a given list of ink docs on a specific doc view + * @param selected: the list of ink docs to create a grouping of + * @param docView: the view in which we want the grouping to be created + * @param word: optional param if the group we are creating is a word (subgrouping individual words) + * @returns a new collection Doc or undefined if the grouping fails + */ + groupInkDocs(selected: Doc[], docView: CollectionFreeFormView, word?: string): Doc | undefined { + this.collectionFreeForm = docView; + const bounds: { x: number; y: number; width?: number; height?: number }[] = []; + + // calculate the necessary bounds from the selected ink docs + selected.map( + action(d => { + const x = NumCast(d.x); + const y = NumCast(d.y); + const width = NumCast(d._width); + const height = NumCast(d._height); + bounds.push({ x, y, width, height }); + }) + ); + + // calculate the aggregated bounds + const aggregBounds = aggregateBounds(bounds, 0, 0); + const marqViewRef = docView._marqueeViewRef.current; + + // set the vals for bounds in marqueeView + if (marqViewRef) { + marqViewRef._downX = aggregBounds.x; + marqViewRef._downY = aggregBounds.y; + marqViewRef._lastX = aggregBounds.r; + marqViewRef._lastY = aggregBounds.b; + } + + // map through all the selected ink strokes and create the groupings + selected.map( + action(d => { + const dx = NumCast(d.x); + const dy = NumCast(d.y); + delete d.x; + delete d.y; + delete d.activeFrame; + delete d._timecodeToShow; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + delete d._timecodeToHide; // bcz: this should be automatic somehow.. along with any other properties that were logically associated with the original collection + // calculate pos based on bounds + if (marqViewRef?.Bounds) { + d.x = dx - marqViewRef.Bounds.left - marqViewRef.Bounds.width / 2; + d.y = dy - marqViewRef.Bounds.top - marqViewRef.Bounds.height / 2; + } + return d; + }) + ); + docView.props.removeDocument?.(selected); + // Gets a collection based on the selected nodes using a marquee view ref + const newCollection = marqViewRef?.getCollection(selected, undefined, true); + if (newCollection) { + newCollection.width = NumCast(newCollection._width); + newCollection.height = NumCast(newCollection._height); + // if the grouping we are creating is an individual word + if (word) { + newCollection.title = word; + } + } + + // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs + newCollection && docView.props.addDocument?.(newCollection); + if (newCollection) { + newCollection.hasTextBox = false; + } + return newCollection; + } + + 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> + ); + } +} diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 55f28f415..ce1c07f2f 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -104,8 +104,8 @@ 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() { - const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; + analyzeStrokes=()=> { + const data: InkData = this.inkScaledData().inkData ?? []; CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ['inkAnalysis', 'handwriting'], [data]); } diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index 7198c7f05..a0284e55a 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -12,7 +12,7 @@ import { emptyFunction } from '../../Utils'; import { CreateLinkToActiveAudio, Doc, DocListCast, FieldResult, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkTool } from '../../fields/InkField'; -import { Cast, NumCast, toList } from '../../fields/Types'; +import { Cast, DocCast, NumCast, toList } from '../../fields/Types'; import { SnappingManager } from '../util/SnappingManager'; import { Transform } from '../util/Transform'; import { GestureOverlay } from './GestureOverlay'; @@ -23,6 +23,8 @@ import { DocumentView } from './nodes/DocumentView'; import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; import { OverlayView } from './OverlayView'; +import { AnnotationPalette } from './smartdraw/AnnotationPalette'; +import { DocData } from '../../fields/DocSymbols'; interface LightboxViewProps { PanelWidth: number; @@ -40,7 +42,12 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> { * @param view * @returns true if a DocumentView is descendant of the lightbox view */ - public static Contains(view?:DocumentView) { return view && LightboxView.Instance?._docView && (view.containerViewPath?.() ?? []).concat(view).includes(LightboxView.Instance?._docView); } // prettier-ignore + public static Contains(view?: DocumentView) { + return ( + (view && LightboxView.Instance?._docView && (view.containerViewPath?.() ?? []).concat(view).includes(LightboxView.Instance?._docView)) || + (view && LightboxView.Instance?._annoPaletteView?.Contains(view)) + ); + } // prettier-ignore public static LightboxDoc = () => LightboxView.Instance?._doc; // eslint-disable-next-line no-use-before-define static Instance: LightboxView; @@ -59,6 +66,8 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> { @observable private _doc: Opt<Doc> = undefined; @observable private _docTarget: Opt<Doc> = undefined; @observable private _docView: Opt<DocumentView> = undefined; + @observable private _showPalette: boolean = false; + private _annoPaletteView: AnnotationPalette | null = null; @computed get leftBorder() { return Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]); } // prettier-ignore @computed get topBorder() { return Math.min(this._props.PanelHeight / 4, this._props.maxBorder[1]); } // prettier-ignore @@ -71,6 +80,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> { DocumentView._lightboxContains = LightboxView.Contains; DocumentView._lightboxDoc = LightboxView.LightboxDoc; } + /** * Sets the root Doc to render in the lightbox view. * @param doc @@ -103,6 +113,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> { this._history = []; Doc.ActiveTool = InkTool.None; SnappingManager.SetExploreMode(false); + this._showPalette = false; } DocumentView.DeselectAll(); if (future) { @@ -202,6 +213,9 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> { toggleFitWidth = () => { this._doc && (this._doc._layout_fitWidth = !this._doc._layout_fitWidth); }; + togglePalette = () => { + this._showPalette = !this._showPalette; + }; togglePen = () => { Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen; }; @@ -306,6 +320,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> { </GestureOverlay> </div> + {this._showPalette && <AnnotationPalette ref={r => (this._annoPaletteView = r)} Document={DocCast(Doc.UserDoc().myLightboxDrawings)} />} {this.renderNavBtn(0, undefined, this._props.PanelHeight / 2 - 12.5, 'chevron-left', this._doc && this._history.length, this.previous)} {this.renderNavBtn( this._props.PanelWidth - Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]), @@ -318,7 +333,8 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> { )} <LightboxTourBtn lightboxDoc={this.lightboxDoc} navBtn={this.renderNavBtn} future={this.future} stepInto={this.stepInto} /> {toggleBtn('lightboxView-navBtn', 'toggle reading view', this._doc?._layout_fitWidth, 'book-open', 'book', this.toggleFitWidth)} - {toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-download', '', this.downloadDoc)} + {toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-export', '', this.downloadDoc)} + {toggleBtn('lightboxView-paletteBtn', 'toggle annotation palette', this._showPalette === true, 'palette', '', this.togglePalette)} {toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Pen, 'pen', '', this.togglePen)} {toggleBtn('lightboxView-exploreBtn', 'toggle navigate only mode', SnappingManager.ExploreMode, 'globe-americas', '', this.toggleExplore)} </div> diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index ef1bcfb64..e6d6b20a4 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -76,6 +76,9 @@ import { PresBox } from './nodes/trails'; import { AnchorMenu } from './pdf/AnchorMenu'; import { GPTPopup } from './pdf/GPTPopup/GPTPopup'; import { TopBar } from './topbar/TopBar'; +import { SmartDrawHandler } from './smartdraw/SmartDrawHandler'; +import { AnnotationPalette } from './smartdraw/AnnotationPalette'; +import { InkTranscription } from './InkTranscription'; const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore const _global = (window /* browser */ || global) /* node */ as any; @@ -339,6 +342,7 @@ export class MainView extends ObservableReactComponent<{}> { fa.faTerminal, fa.faToggleOn, fa.faFile, + fa.faFileExport, fa.faLocationArrow, fa.faSearch, fa.faFileDownload, @@ -378,6 +382,7 @@ export class MainView extends ObservableReactComponent<{}> { fa.faXmark, fa.faExclamation, fa.faFileAlt, + fa.faFileArrowDown, fa.faFileAudio, fa.faFileVideo, fa.faFilePdf, @@ -431,6 +436,7 @@ export class MainView extends ObservableReactComponent<{}> { fa.faBold, fa.faItalic, fa.faClipboard, + fa.faClipboardCheck, fa.faUnderline, fa.faStrikethrough, fa.faSuperscript, @@ -477,6 +483,8 @@ export class MainView extends ObservableReactComponent<{}> { fa.faHashtag, fa.faAlignJustify, fa.faCheckSquare, + fa.faSquarePlus, + fa.faReply, fa.faListUl, fa.faWindowMinimize, fa.faWindowRestore, @@ -572,6 +580,9 @@ export class MainView extends ObservableReactComponent<{}> { Doc.linkFollowUnhighlight(); AudioBox.Enabled = true; const targets = document.elementsFromPoint(e.x, e.y); + const targetClasses: string[] = targets.map(target => { + return target.className.toString(); + }); if (targets.length) { let targClass = targets[0].className.toString(); for (let i = 0; i < targets.length - 1; i++) { @@ -579,6 +590,8 @@ export class MainView extends ObservableReactComponent<{}> { else break; } !targClass.includes('contextMenu') && ContextMenu.Instance.closeMenu(); + !targetClasses.includes('marqueeView') && !targetClasses.includes('smart-draw-handler') && SmartDrawHandler.Instance.hideSmartDrawHandler(); + !targetClasses.includes('smart-draw-handler') && SmartDrawHandler.Instance.hideRegenerate(); !['timeline-menu-desc', 'timeline-menu-item', 'timeline-menu-input'].includes(targClass) && TimelineMenu.Instance.closeMenu(); } }); @@ -1096,6 +1109,7 @@ export class MainView extends ObservableReactComponent<{}> { <MarqueeOptionsMenu /> <TimelineMenu /> <RichTextMenu /> + <InkTranscription /> {this.snapLines} <LightboxView key="lightbox" PanelWidth={this._windowWidth} addSplit={CollectionDockingView.AddSplit} PanelHeight={this._windowHeight} maxBorder={this.lightboxMaxBorder} /> <GPTPopup key="gptpopup" /> diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts index c633f34fb..ea5f3dd27 100644 --- a/src/client/views/ViewBoxInterface.ts +++ b/src/client/views/ViewBoxInterface.ts @@ -19,7 +19,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React abstract get dataDoc(): Doc; abstract get fieldKey(): string; promoteCollection?: () => void; // moves contents of collection to parent - updateIcon?: () => void; // updates the icon representation of the document + updateIcon?: (usePanelDimensions?: boolean) => void; // updates the icon representation of the document getAnchor?: (addAsAnnotation: boolean, pinData?: PinProps) => Doc; // returns an Anchor Doc that represents the current state of the doc's componentview (e.g., the current playhead location of a an audio/video box) restoreView?: (viewSpec: Doc) => boolean; scrollPreview?: (docView: DocumentView, doc: Doc, focusSpeed: number, options: FocusViewOptions) => Opt<number>; // returns the duration of the focus diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index 38f681e87..4f3ce9d9b 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -79,7 +79,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { // suppressSetHeight={true} NativeWidth={returnZero} NativeHeight={returnZero} - fitWidth={undefined} + fitWidth={this._props.childLayoutFitWidth} onDoubleClickScript={this.onChildDoubleClick} renderDepth={this._props.renderDepth + 1} LayoutTemplate={this._props.childLayoutTemplate} diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 2adad68e0..8b083de15 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -8,7 +8,7 @@ import * as React from 'react'; import { StopEvent, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils'; import { emptyFunction } from '../../../Utils'; import { Doc, Opt } from '../../../fields/Doc'; -import { DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; +import { Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { ContextMenu } from '../ContextMenu'; @@ -144,6 +144,7 @@ export class CollectionCarouselView extends CollectionSubView() { revealItems.push({description: 'Quiz Cards', event: () => {this.layoutDoc.filterOp = cardMode.QUIZ;}, icon: 'pencil',}); // prettier-ignore !revealOptions && cm.addItem({ description: 'Filter Flashcards', addDivider: false, noexpand: true, subitems: revealItems, icon: 'layer-group' }); }; + childFitWidth = (doc: Doc) => Cast(this.Document.childLayoutFitWidth, 'boolean', this._props.childLayoutFitWidth?.(doc) ?? Cast(doc.layout_fitWidth, 'boolean', null)); @computed get content() { const index = NumCast(this.layoutDoc._carousel_index); const curDoc = this.carouselItems?.[index]; @@ -154,10 +155,11 @@ export class CollectionCarouselView extends CollectionSubView() { <div className="collectionCarouselView-image" key="image"> <DocumentView {...this._props} - NativeWidth={returnZero} - NativeHeight={returnZero} - fitWidth={undefined} + // NativeWidth={returnZero} + // NativeHeight={returnZero} + fitWidth={returnTrue} setContentViewBox={undefined} + containerViewPath={this.DocumentView?.().docViewPath} onDoubleClickScript={this.onContentDoubleClick} onClickScript={this.onContentClick} isDocumentActive={this._props.childDocumentsActive?.() ? this._props.isDocumentActive : this._props.isContentActive} @@ -169,6 +171,7 @@ export class CollectionCarouselView extends CollectionSubView() { Document={curDoc.layout} TemplateDataDocument={DocCast(curDoc.layout.resolvedDataDoc)} PanelHeight={this.panelHeight} + PanelWidth={this._props.PanelWidth} /> </div> {!carouselShowsCaptions ? null : ( diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 5b7f09be3..62632e8c2 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -55,6 +55,8 @@ import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannable import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors'; import './CollectionFreeFormView.scss'; import { MarqueeView } from './MarqueeView'; +import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler'; +import { AnnotationPalette } from '../../smartdraw/AnnotationPalette'; @observer class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> { @@ -1239,6 +1241,69 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; @action + showSmartDraw = (e: PointerEvent, doubleTap?: boolean) => { + SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createDrawing, this.removeDrawing); + }; + + _drawing: Doc[] = []; + _drawingContainer: Doc | undefined = undefined; + @undoBatch + createDrawing = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => { + 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); + const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale; + const inkDoc = Docs.Create.InkDocument( + stroke[0], + { title: 'stroke', + x: B.x - inkWidth / 2, + y: B.y - inkWidth / 2, + _width: B.width + inkWidth, + _height: B.height + inkWidth, + stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore + inkWidth, + opts.autoColor ? stroke[1] : ActiveInkColor(), + ActiveInkBezierApprox(), + stroke[2] === 'none' ? ActiveFillColor() : stroke[2], + ActiveArrowStart(), + ActiveArrowEnd(), + ActiveDash(), + ActiveIsInkMask() + ); + this._drawing.push(inkDoc); + 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(); + }; + + removeDrawing = (doc?: Doc) => { + this._batch = UndoManager.StartBatch('regenerateDrawing'); + if (doc) { + const docData = doc[DocData]; + const children = DocListCast(docData.data); + this._props.removeDocument?.(doc); + this._props.removeDocument?.(children); + } else { + if (this._drawingContainer) this._props.removeDocument?.(this._drawingContainer); + } + this._drawing = []; + }; + + @action zoom = (pointX: number, pointY: number, deltaY: number): void => { if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return; let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05; @@ -1688,7 +1753,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection .forEach(entry => elements.push({ ele: this.getChildDocView(entry[1]), - bounds: (entry[1].opacity === 0 ? { payload:undefined, type:"", ...entry[1], width: 0, height: 0 } : { payload:undefined, type:"",...entry[1] }), + bounds: entry[1].opacity === 0 ? { payload: undefined, type: '', ...entry[1], width: 0, height: 0 } : { payload: undefined, type: '', ...entry[1] }, inkMask: BoolCast(entry[1].pair.layout.stroke_isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1, }) ); @@ -1810,26 +1875,27 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection Object.values(this._disposers).forEach(disposer => disposer?.()); } - updateIcon = () => { + updateIcon = (usePanelDimensions?: boolean) => { const contentDiv = this.DocumentView?.().ContentDiv; - contentDiv && UpdateIcon( - this.layoutDoc[Id] + '-icon' + new Date().getTime(), - contentDiv, - NumCast(this.layoutDoc._width), - NumCast(this.layoutDoc._height), - this._props.PanelWidth(), - this._props.PanelHeight(), - 0, - 1, - false, - '', - (iconFile, nativeWidth, nativeHeight) => { - this.dataDoc.icon = new ImageField(iconFile); - this.dataDoc.icon_nativeWidth = nativeWidth; - this.dataDoc.icon_nativeHeight = nativeHeight; - } - ); - } + contentDiv && + UpdateIcon( + this.layoutDoc[Id] + '-icon' + new Date().getTime(), + contentDiv, + usePanelDimensions ? this._props.PanelWidth() : NumCast(this.layoutDoc._width), + usePanelDimensions ? this._props.PanelHeight() : NumCast(this.layoutDoc._height), + this._props.PanelWidth(), + this._props.PanelHeight(), + 0, + 1, + false, + '', + (iconFile, nativeWidth, nativeHeight) => { + this.dataDoc.icon = new ImageField(iconFile); + this.dataDoc.icon_nativeWidth = nativeWidth; + this.dataDoc.icon_nativeHeight = nativeHeight; + } + ); + }; @action onCursorMove = (e: React.PointerEvent) => { @@ -1941,6 +2007,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }), icon: 'eye', }); + 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(); + }), + icon: 'pen-to-square', + }); this._props.renderDepth && optionItems.push({ description: 'Use Background Color as Default', diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx index f02cd9d45..b3fdd9379 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx @@ -3,6 +3,7 @@ import { IconButton } from 'browndash-components'; import { computed, makeObservable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; +import { Doc } from '../../../../fields/Doc'; import { unimplementedFunction } from '../../../../Utils'; import { SettingsManager } from '../../../util/SettingsManager'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; @@ -12,7 +13,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> { // eslint-disable-next-line no-use-before-define static Instance: MarqueeOptionsMenu; - public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean) => void = unimplementedFunction; + public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean, selection?: Doc[]) => Doc | void = unimplementedFunction; public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction; public showMarquee: () => void = unimplementedFunction; diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index dc15c83c5..63e36fc31 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -362,7 +362,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps this.hideMarquee(); }); - getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, makeGroup: Opt<boolean>) => { + public static getCollection = action((selected: Doc[], creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, makeGroup: Opt<boolean>, bounds: MarqueeViewBounds) => { const newCollection = creator ? creator(selected, { title: 'nested stack' }) : ((doc: Doc) => { @@ -374,14 +374,13 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); newCollection.isSystem = undefined; - newCollection._width = this.Bounds.width; - newCollection._height = this.Bounds.height; + newCollection._width = bounds.width || 1; // if width/height are unset/0, then groups won't autoexpand to contain their children + newCollection._height = bounds.height || 1; newCollection._dragWhenActive = makeGroup; - newCollection.x = this.Bounds.left; - newCollection.y = this.Bounds.top; + newCollection.x = bounds.left; + newCollection.y = bounds.top; newCollection.layout_fitWidth = true; selected.forEach(d => Doc.SetContainer(d, newCollection)); - this.hideMarquee(); return newCollection; }); @@ -418,7 +417,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps this._props.removeDocument?.(selected); } - const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group); + const newCollection = MarqueeView.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group, this.Bounds); newCollection._freeform_panX = this.Bounds.left + this.Bounds.width / 2; newCollection._freeform_panY = this.Bounds.top + this.Bounds.height / 2; newCollection._currentFrame = activeFrame; diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 2b7de5082..bba34e302 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -35,6 +35,7 @@ import { ImageBox } from '../nodes/ImageBox'; import { VideoBox } from '../nodes/VideoBox'; import { WebBox } from '../nodes/WebBox'; import { RichTextMenu } from '../nodes/formattedText/RichTextMenu'; +import { InkTranscription } from '../InkTranscription'; // import { InkTranscription } from '../InkTranscription'; @@ -364,6 +365,7 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?: export function createInkGroup(/* inksToGroup?: Doc[], isSubGroup?: boolean */) { // TODO nda - if document being added to is a inkGrouping then we can just add to that group if (Doc.ActiveTool === InkTool.Write) { + console.log('create inking group '); CollectionFreeFormView.collectionsWithUnprocessedInk.forEach(ffView => { // TODO: nda - will probably want to go through ffView unprocessed docs and then see if any of the inksToGroup docs are in it and only use those const selected = ffView.unprocessedDocs; @@ -421,14 +423,14 @@ export function createInkGroup(/* inksToGroup?: Doc[], isSubGroup?: boolean */) // TODO: nda - will probably need to go through and only remove the unprocessed selected docs ffView.unprocessedDocs = []; - // InkTranscription.Instance.transcribeInk(newCollection, selected, false); + InkTranscription.Instance.transcribeInk(newCollection, selected, false); }); } CollectionFreeFormView.collectionsWithUnprocessedInk.clear(); } function setActiveTool(tool: InkTool | Gestures, keepPrim: boolean, checkResult?: boolean) { - // InkTranscription.Instance?.createInkGroup(); + InkTranscription.Instance?.createInkGroup(); if (checkResult) { return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool ? GestureOverlay.Instance?.KeepPrimitiveMode || ![Gestures.Circle, Gestures.Line, Gestures.Rectangle].includes(tool as Gestures) diff --git a/src/client/views/nodes/DiagramBox.scss b/src/client/views/nodes/DiagramBox.scss index d2749f1ad..4bfd4f7cb 100644 --- a/src/client/views/nodes/DiagramBox.scss +++ b/src/client/views/nodes/DiagramBox.scss @@ -1,87 +1,229 @@ -.DIYNodeBox { +.DiagramBox { + overflow:hidden; width: 100%; height: 100%; display: flex; flex-direction: column; - align-items: center; - justify-content: center; - - .DIYNodeBox-wrapper { + .buttonCollections{ + display: flex; + justify-content: center; + flex-direction: column; + height:100%; + padding:20px; + padding-right:40px; + button{ + font-size:15px; + height:100%; + width:100%; + border: none; + border-radius: 5px; + cursor: pointer; + background-color: #007bff; + color: #fff; + transition: background-color 0.3s ease; + } + button:hover { + background-color: #0056b3; + } + + } + .DiagramBox-wrapper { + overflow:hidden; width: 100%; height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: center; - .DIYNodeBox { - /* existing code */ - - .DIYNodeBox-iframe { - height: 100%; - width: 100%; - border: none; - - } - } - - .search-bar { + .contentCode{ + overflow: hidden; display: flex; justify-content: center; align-items: center; - width: 100%; - padding: 10px; - - input[type="text"] { - flex: 1; - margin-right: 10px; + flex-direction:row; + padding:10px; + width:100%; + height:100%; + .topbar{ + .backButtonDrawing{ + padding: 5px 10px; + height:23px; + border-radius: 10px; + text-align: center; + padding:0; + width:50px; + font-size:10px; + position:absolute; + top:10px; + left:10px; + } + p{ + margin-left:60px + } } + .search-bar { + overflow:hidden; + position:absolute; + top:0; + .backButton{ + text-align: center; + padding:0; + width:50px; + font-size:10px; - button { - padding: 5px 10px; + } + .exampleButton{ + width:100px; + height:30px; + } + display: flex; + flex-wrap: wrap; + justify-content: space-between; + align-items: center; + width: 100%; + button { + padding: 5px 10px; + width:80px; + height:23px; + border-radius: 3px; + } + } + .exampleButtonContainer{ + display:flex; + flex-direction: column; + position: absolute; + top:37px; + right:30px; + width:50px; + z-index: 200; + button{ + width:70px; + margin:2px; + padding:0px; + height:15px; + border-radius: 3px; + } + } + textarea { + position:relative; + width:40%; + height: 100%; + height: calc(100% - 25px); + top:15px; + resize:none; + overflow: hidden; + } + .diagramBox{ + flex: 1; + display: flex; + justify-content: center; + align-items: center; + svg{ + position: relative; + top:25; + max-width: none !important; + height: calc(100% - 50px); + } } } - .content { - flex: 1; + overflow: hidden; display: flex; justify-content: center; align-items: center; + flex-direction: column; + padding:10px; width:100%; height:100%; - .diagramBox{ - flex: 1; + .topbar{ + .backButtonDrawing{ + padding: 5px 10px; + height:23px; + border-radius: 10px; + text-align: center; + padding:0; + width:50px; + font-size:10px; + position:absolute; + top:10px; + left:10px; + } + p{ + margin-left:60px + } + } + .search-bar { + overflow:hidden; + position:absolute; + top:0; + .backButton{ + text-align: center; + padding:0; + width:50px; + font-size:10px; + + } display: flex; + flex-wrap: wrap; justify-content: center; align-items: center; - width:100%; - height:100%; - svg{ + width: 100%; + textarea { flex: 1; - display: flex; - justify-content: center; - align-items: center; - width:100%; - height:100%; + height: 5px; + min-height: 20px; + resize:none; + overflow: hidden; + } + button { + padding: 5px 10px; + width:80px; + height:23px; + border-radius: 10px; + } + .rightButtons{ + display:flex; + flex-direction: column; + button { + padding: 5px 10px; + width:80px; + height:23px; + margin:2; + border-radius: 10px; + } } } - } - - .loading-circle { - position: relative; - width: 50px; - height: 50px; - border-radius: 50%; - border: 3px solid #ccc; - border-top-color: #333; - animation: spin 1s infinite linear; - } - - @keyframes spin { - 0% { - transform: rotate(0deg); + .loading-circle { + position: absolute; + display:flex; + align-items: center; + justify-content: center; + width: 50px; + height: 50px; + border-radius: 50%; + border: 3px solid #ccc; + border-top-color: #333; + animation: spin 1s infinite linear; } - 100% { - transform: rotate(360deg); + @keyframes spin { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } + } + .diagramBox{ + flex: 1; + display: flex; + justify-content: center; + align-items: center; + svg{ + position: relative; + top:25; + max-width: none !important; + height: calc(100% - 50px); + } } } } diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx index 32969fa53..5a712b8b0 100644 --- a/src/client/views/nodes/DiagramBox.tsx +++ b/src/client/views/nodes/DiagramBox.tsx @@ -1,11 +1,13 @@ +/* eslint-disable prettier/prettier */ +/* eslint-disable jsx-a11y/control-has-associated-label */ import mermaid from 'mermaid'; -import { action, makeObservable, observable, reaction } from 'mobx'; +import { action, makeObservable, observable, reaction, computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, DocListCast } from '../../../fields/Doc'; import { List } from '../../../fields/List'; import { RichTextField } from '../../../fields/RichTextField'; -import { DocCast, NumCast } from '../../../fields/Types'; +import { DocCast, BoolCast } from '../../../fields/Types'; import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT'; import { DocumentType } from '../../documents/DocumentTypes'; import { Docs } from '../../documents/Documents'; @@ -15,6 +17,16 @@ import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { InkingStroke } from '../InkingStroke'; import './DiagramBox.scss'; import { FieldView, FieldViewProps } from './FieldView'; +import { PointData } from '../../../pen-gestures/GestureTypes'; +import { InkField } from '../../../fields/InkField'; + +enum menuState { + option, + mermaidCode, + drawing, + gpt, + justCreated, +} @observer export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @@ -27,60 +39,97 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { super(props); makeObservable(this); } - + @observable menuState = menuState.justCreated; + @observable renderDiv: React.ReactNode; @observable inputValue = ''; + @observable createInputValue = ''; @observable loading = false; @observable errorMessage = ''; @observable mermaidCode = ''; + @observable isExampleMenuOpen = false; - @action handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { + @action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { this.inputValue = e.target.value; + console.log(e.target.value); }; async componentDidMount() { this._props.setContentViewBox?.(this); mermaid.initialize({ securityLevel: 'loose', startOnLoad: true, - flowchart: { useMaxWidth: true, htmlLabels: true, curve: 'cardinal' }, + darkMode: true, + flowchart: { useMaxWidth: false, htmlLabels: true, curve: 'cardinal' }, + gantt: { useMaxWidth: true, useWidth: 2000 }, }); - this.mermaidCode = 'asdasdasd'; - const docArray: Doc[] = DocListCast(this.Document.data); - let mermaidCodeDoc = docArray.filter(doc => doc.type === 'rich text'); - mermaidCodeDoc = mermaidCodeDoc.filter(doc => (doc.text as RichTextField).Text === 'mermaidCodeTitle'); - if (mermaidCodeDoc[0]) { - if (typeof mermaidCodeDoc[0].title === 'string') { - console.log(mermaidCodeDoc[0].title); - if (mermaidCodeDoc[0].title !== '') { - this.renderMermaidAsync(mermaidCodeDoc[0].title); - } - } + if (!this.Document.testValue) { + this.Document.height = 500; + this.Document.width = 500; } - // this will create a text doc far away where the user cant to save the mermaid code, where it will then be accessed when flipped to the diagram box side - // the code is stored in the title since it is much easier to change than in the text - else { - DocumentManager.Instance.AddViewRenderedCb(this.Document, docViewForYourCollection => { - if (docViewForYourCollection && docViewForYourCollection.ComponentView) { - if (docViewForYourCollection.ComponentView.addDocument && docViewForYourCollection.ComponentView.removeDocument) { - const newDoc = Docs.Create.TextDocument('mermaidCodeTitle', { title: '', x: 9999 + NumCast(this.layoutDoc._width), y: 9999 }); - docViewForYourCollection.ComponentView?.addDocument(newDoc); - } - } - }); + this.Document.testValue = 'a'; + this.mermaidCode = 'a'; + if (typeof this.Document.drawingMermaidCode === 'string' && this.Document.menuState === 'drawing') { + this.renderMermaidAsync(this.Document.drawingMermaidCode); } - console.log(this.Document.title); // this is so that ever time a new doc, text node or ink node, is created, this.createMermaidCode will run which will create a save - reaction( - () => DocListCast(this.Document.data), - () => this.convertDrawingToMermaidCode(), - { fireImmediately: true } - ); + // reaction( + // () => DocListCast(this.Document.data), + // () => this.lockInkStroke(), + // { fireImmediately: true } + // ); + // reaction( + // () => + // DocListCast(this.Document.data) + // .filter(doc => doc.type === 'rich text') + // .map(doc => (doc.text as RichTextField).Text), + // () => this.convertDrawingToMermaidCode(), + // { fireImmediately: true } + // ); + // const rectangleXValues = computed(() => + // DocListCast(this.Document.data) + // .filter(doc => doc.title === 'rectangle') + // .map(doc => doc.x) + // ); + // reaction( + // () => rectangleXValues.get(), + // () => this.lockInkStroke(), + // { fireImmediately: true } + // ); + this.lockInkStroke(); + } + + componentDidUpdate = () => { + if (typeof this.Document.drawingMermaidCode === 'string' && this.Document.menuState === 'drawing') { + this.renderMermaidAsync(this.Document.drawingMermaidCode); + } + if (typeof this.Document.gptMermaidCode === 'string' && this.Document.menuState === 'gpt') { + this.renderMermaidAsync(this.Document.gptMermaidCode); + } + }; + switchRenderDiv() { + switch (this.Document.menuState) { + case 'option': + this.renderDiv = this.renderOption(); + break; + case 'drawing': + this.renderDiv = this.renderDrawing(); + break; + case 'gpt': + this.renderDiv = this.renderGpt(); + break; + case 'mermaidCode': + this.renderDiv = this.renderMermaidCode(); + break; + default: + this.menuState = menuState.option; + this.renderDiv = this.renderOption(); + } } renderMermaid = async (str: string) => { try { const { svg, bindFunctions } = await this.mermaidDiagram(str); return { svg, bindFunctions }; } catch (error) { - console.error('Error rendering mermaid diagram:', error); + // console.error('Error rendering mermaid diagram:', error); return { svg: '', bindFunctions: undefined }; } }; @@ -92,6 +141,7 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { const dashDiv = document.getElementById('dashDiv' + this.Document.title); if (dashDiv) { dashDiv.innerHTML = svg; + // this.changeHeightWidth(svg); if (bindFunctions) { bindFunctions(dashDiv); } @@ -100,51 +150,51 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { console.error('Error rendering Mermaid:', error); } } + changeHeightWidth(svgString: string) { + const pattern = /width="([\d.]+)"\s*height="([\d.]+)"/; + + const match = svgString.match(pattern); + + if (match) { + const width = parseFloat(match[1]); + const height = parseFloat(match[2]); + console.log(width); + console.log(height); + this.Document.width = width; + this.Document.height = height; + } + } @action handleRenderClick = () => { - this.generateMermaidCode(); + this.mermaidCode = ''; + if (this.inputValue) { + this.generateMermaidCode(); + } }; @action async generateMermaidCode() { console.log('Generating Mermaid Code'); + const dashDiv = document.getElementById('dashDiv' + this.Document.title); + if (dashDiv) { + dashDiv.innerHTML = ''; + } this.loading = true; let prompt = ''; - // let docArray: Doc[] = DocListCast(this.Document.data); - // let mermaidCodeDoc = docArray.filter(doc => doc.type == 'rich text') - // mermaidCodeDoc=mermaidCodeDoc.filter(doc=>(doc.text as RichTextField).Text=='mermaidCodeTitle') - // if(mermaidCodeDoc[0]){ - // console.log(mermaidCodeDoc[0].title) - // if(typeof mermaidCodeDoc[0].title=='string'){ - // console.log(mermaidCodeDoc[0].title) - // if(mermaidCodeDoc[0].title!=""){ - // prompt="Edit this code "+this.inputValue+": "+mermaidCodeDoc[0].title - // console.log("you have to see me") - // } - // } - // } - // else{ prompt = 'Write this in mermaid code and only give me the mermaid code: ' + this.inputValue; - console.log('there is no text save'); // } const res = await gptAPICall(prompt, GPTCallType.MERMAID); - this.loading = false; + this.loading = true; if (res === 'Error connecting with API.') { // If GPT call failed console.error('GPT call failed'); this.errorMessage = 'GPT call failed; please try again.'; } else if (res !== null) { // If GPT call succeeded, set htmlCode;;; TODO: check if valid html - if (this.isValidCode(res)) { - this.mermaidCode = res; - console.log('GPT call succeeded:' + res); - this.errorMessage = ''; - } else { - console.error('GPT call succeeded but invalid html; please try again.'); - this.errorMessage = 'GPT call succeeded but invalid html; please try again.'; - } + this.mermaidCode = res; + console.log('GPT call succeeded:' + res); + this.errorMessage = ''; } this.renderMermaidAsync.call(this, this.removeWords(this.mermaidCode)); - this.loading = false; + this.Document.gptMermaidCode = this.removeWords(this.mermaidCode); } - isValidCode = (html: string) => true; removeWords(inputStrIn: string) { const inputStr = inputStrIn.replace('```mermaid', ''); return inputStr.replace('```', ''); @@ -164,10 +214,12 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }); await timeoutPromise(); const inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke); - console.log(inkStrokeArray.length); - console.log(lineArray.length); if (inkStrokeArray[0] && inkStrokeArray.length === lineArray.length) { - mermaidCode = 'graph TD;'; + // if (this.isLeftRightDiagram(docArray)) { + // mermaidCode = 'graph LR;'; + // } else { + // mermaidCode = 'graph TD;'; + // } const inkingStrokeArray = inkStrokeArray.map(stroke => stroke?.ComponentView); for (let i = 0; i < rectangleArray.length; i++) { const rectangle = rectangleArray[i]; @@ -182,8 +234,6 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { ?.inkScaledData() .inkData.map(coord => coord.Y) .map(doc => doc * inkScaleY); - console.log(inkingStrokeArray.length); - console.log(lineArray.length); // need to minX and minY to since the inkStroke.x and.y is not relative to the doc. so I have to do some calcluations const minX: number = Math.min(...inkStrokeXArray); const minY: number = Math.min(...inkStrokeYArray); @@ -197,12 +247,11 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { if (this.isPointInBox(rectangle2, [endX, endY]) && typeof rectangle.x === 'number' && typeof rectangle2.x === 'number') { diagramExists = true; const linkedDocs: Doc[] = LinkManager.Instance.getAllRelatedLinks(lineArray[j]).map(d => DocCast(LinkManager.getOppositeAnchor(d, lineArray[j]))); - console.log(linkedDocs.length); if (linkedDocs.length !== 0) { const linkedText = (linkedDocs[0].text as RichTextField).Text; - mermaidCode += Math.abs(rectangle.x) + this.getTextInBox(rectangle, textArray) + '-->|' + linkedText + '|' + Math.abs(rectangle2.x) + this.getTextInBox(rectangle2, textArray) + ';'; + mermaidCode += Math.abs(rectangle.x) + this.getTextInBox(rectangle, textArray) + '---|' + linkedText + '|' + Math.abs(rectangle2.x) + this.getTextInBox(rectangle2, textArray) + ';'; } else { - mermaidCode += Math.abs(rectangle.x) + this.getTextInBox(rectangle, textArray) + '-->' + Math.abs(rectangle2.x) + this.getTextInBox(rectangle2, textArray) + ';'; + mermaidCode += Math.abs(rectangle.x) + this.getTextInBox(rectangle, textArray) + '---' + Math.abs(rectangle2.x) + this.getTextInBox(rectangle2, textArray) + ';'; } } } @@ -210,35 +259,166 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } } // this will save the text - DocumentManager.Instance.AddViewRenderedCb(this.Document, docViewForYourCollection => { - if (docViewForYourCollection && docViewForYourCollection.ComponentView) { - if (docViewForYourCollection.ComponentView.addDocument && docViewForYourCollection.ComponentView.removeDocument) { - let docs: Doc[] = DocListCast(this.Document.data); - docs = docs.filter(doc => doc.type === 'rich text'); - const mermaidCodeDoc = docs.filter(doc => (doc.text as RichTextField).Text === 'mermaidCodeTitle'); - if (mermaidCodeDoc[0]) { - if (diagramExists) { - mermaidCodeDoc[0].title = mermaidCode; - } else { - mermaidCodeDoc[0].title = ''; - } - } - } - } - }); + if (diagramExists) { + this.Document.drawingMermaidCode = mermaidCode; + } else { + this.Document.drawingMermaidCode = ''; + } } } } - testInkingStroke = () => { + async lockInkStroke() { + console.log('hello'); + console.log( + DocListCast(this.Document.data) + .filter(doc => doc.title === 'rectangle') + .map(doc => doc.x) + ); if (this.Document.data instanceof List) { const docArray: Doc[] = DocListCast(this.Document.data); + const rectangleArray = docArray.filter(doc => doc.title === 'rectangle' || doc.title === 'circle'); + if (rectangleArray[0]) { + console.log(rectangleArray[0].x); + } const lineArray = docArray.filter(doc => doc.title === 'line' || doc.title === 'stroke'); - setTimeout(() => { - const inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke); - console.log(inkStrokeArray); - }); + const timeoutPromise = () => + new Promise(resolve => { + setTimeout(resolve, 0); + }); + await timeoutPromise(); + const inkStrokeArray = lineArray.map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke); + const inkingStrokeArray = inkStrokeArray.map(stroke => stroke?.ComponentView); + for (let j = 0; j < lineArray.length; j++) { + const inkScaleX = (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkScaleX; + const inkScaleY = (inkingStrokeArray[j] as InkingStroke)?.inkScaledData().inkScaleY; + const inkStrokeXArray = (inkingStrokeArray[j] as InkingStroke) + ?.inkScaledData() + .inkData.map(coord => coord.X) + .map(doc => doc * inkScaleX); + const inkStrokeYArray = (inkingStrokeArray[j] as InkingStroke) + ?.inkScaledData() + .inkData.map(coord => coord.Y) + .map(doc => doc * inkScaleY); + // need to minX and minY to since the inkStroke.x and.y is not relative to the doc. so I have to do some calcluations + const minX: number = Math.min(...inkStrokeXArray); + const minY: number = Math.min(...inkStrokeYArray); + const startX = inkStrokeXArray[0] - minX + (lineArray[j]?.x as number); + const startY = inkStrokeYArray[0] - minY + (lineArray[j]?.y as number); + const endX = inkStrokeXArray[inkStrokeXArray.length - 1] - minX + (lineArray[j].x as number); + const endY = inkStrokeYArray[inkStrokeYArray.length - 1] - minY + (lineArray[j].y as number); + let closestStartRect: Doc = lineArray[0]; + let closestStartDistance = 9999999; + let closestEndRect: Doc = lineArray[0]; + let closestEndDistance = 9999999; + rectangleArray.forEach(rectangle => { + const midPoint = this.getMidPoint(rectangle); + if (this.euclideanDistance(midPoint.X, midPoint.Y, startX, startY) < closestStartDistance && this.euclideanDistance(midPoint.X, midPoint.Y, endX, endY) < closestEndDistance) { + if (this.euclideanDistance(midPoint.X, midPoint.Y, startX, startY) < this.euclideanDistance(midPoint.X, midPoint.Y, endX, endY)) { + closestStartDistance = this.euclideanDistance(midPoint.X, midPoint.Y, startX, startY); + closestStartRect = rectangle; + } else { + closestEndDistance = this.euclideanDistance(midPoint.X, midPoint.Y, startX, startY); + closestEndRect = rectangle; + } + } else if (this.euclideanDistance(midPoint.X, midPoint.Y, startX, startY) < closestStartDistance) { + closestStartDistance = this.euclideanDistance(midPoint.X, midPoint.Y, startX, startY); + closestStartRect = rectangle; + } else if (this.euclideanDistance(midPoint.X, midPoint.Y, endX, endY) < closestEndDistance) { + closestEndDistance = this.euclideanDistance(midPoint.X, midPoint.Y, startX, startY); + closestEndRect = rectangle; + } + }); + const inkToDelete: Doc = lineArray[j]; + if ( + typeof closestStartRect.x === 'number' && + typeof closestStartRect.y === 'number' && + typeof closestEndRect.x === 'number' && + typeof closestEndRect.y === 'number' && + typeof closestStartRect.width === 'number' && + typeof closestStartRect.height === 'number' && + typeof closestEndRect.height === 'number' && + typeof closestEndRect.width === 'number' + ) { + const points: PointData[] = [ + { X: closestStartRect.x, Y: closestStartRect.y }, + { X: closestStartRect.x, Y: closestStartRect.y }, + { X: closestEndRect.x, Y: closestEndRect.y }, + { X: closestEndRect.x, Y: closestEndRect.y }, + ]; + let inkX = 0; + let inkY = 0; + if (this.getMidPoint(closestEndRect).X < this.getMidPoint(closestStartRect).X) { + inkX = this.getMidPoint(closestEndRect).X; + } else { + inkX = this.getMidPoint(closestStartRect).X; + } + if (this.getMidPoint(closestEndRect).Y < this.getMidPoint(closestStartRect).Y) { + inkY = this.getMidPoint(closestEndRect).Y; + } else { + inkY = this.getMidPoint(closestStartRect).Y; + } + const newInkDoc = Docs.Create.AudioDocument(''); // get rid of this!! + // const newInkDoc:Doc=Docs.Create.InkDocument( + // points, + // { title: 'stroke', + // x: inkX, + // y: inkY, + // strokeWidth: Math.abs(closestEndRect.x+closestEndRect.width/2-closestStartRect.x-closestStartRect.width/2), + // _height: Math.abs(closestEndRect.y+closestEndRect.height/2-closestStartRect.y-closestStartRect.height/2), + // stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore + // 1) + + DocumentManager.Instance.AddViewRenderedCb(this.Document, docViewForYourCollection => { + if (docViewForYourCollection && docViewForYourCollection.ComponentView) { + if (docViewForYourCollection.ComponentView.addDocument && docViewForYourCollection.ComponentView.removeDocument) { + docViewForYourCollection.ComponentView?.removeDocument(inkToDelete); + docViewForYourCollection.ComponentView?.addDocument(newInkDoc); + + // const bruh2= DocListCast(this.Document.data).filter(doc => doc.title === 'line' || doc.title === 'stroke').map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())).filter(inkView => inkView?.ComponentView instanceof InkingStroke).map(stroke => stroke?.ComponentView); + // console.log(bruh2) + // console.log((bruh2[0] as InkingStroke)?.inkScaledData()) + } + } + }); + } + } } - }; + } + getMidPoint(rectangle: Doc) { + let midPoint = { X: 0, Y: 0 }; + if (typeof rectangle.x === 'number' && typeof rectangle.width === 'number' && typeof rectangle.y === 'number' && typeof rectangle.height === 'number') { + midPoint = { X: rectangle.x + rectangle.width / 2, Y: rectangle.y + rectangle.height / 2 }; + } + return midPoint; + } + euclideanDistance(x1: number, y1: number, x2: number, y2: number): number { + const deltaX = x2 - x1; + const deltaY = y2 - y1; + return Math.sqrt(deltaX * deltaX + deltaY * deltaY); + } + // isLeftRightDiagram = (docArray: Doc[]) => { + // const filteredDocs = docArray.filter(doc => doc.title === 'rectangle' || doc.title === 'circle'); + // const xDoc = filteredDocs.map(doc => doc.x) as number[]; + // const minX = Math.min(...xDoc); + // const xWidthDoc = filteredDocs.map(doc => { + // if (typeof doc.x === 'number' && typeof doc.width === 'number') { + // return doc.x + doc.width; + // } + // }) as number[]; + // const maxX = Math.max(...xWidthDoc); + // const yDoc = filteredDocs.map(doc => doc.y) as number[]; + // const minY = Math.min(...yDoc); + // const yHeightDoc = filteredDocs.map(doc => { + // if (typeof doc.x === 'number' && typeof doc.width === 'number') { + // return doc.x + doc.width; + // } + // }) as number[]; + // const maxY = Math.max(...yHeightDoc); + // if (maxX - minX > maxY - minY) { + // return true; + // } + // return false; + // }; getTextInBox = (box: Doc, richTextArray: Doc[]): string => { for (let i = 0; i < richTextArray.length; i++) { const textDoc = richTextArray[i]; @@ -261,31 +441,262 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } return false; }; + drawingButton = () => { + this.Document.menuState = 'drawing'; + }; + gptButton = () => { + this.Document.menuState = 'gpt'; + }; + mermaidButton = () => { + this.Document.menuState = 'mermaidCode'; + }; + optionButton = () => { + this.Document.menuState = 'option'; + }; + renderOption(): React.ReactNode { + return ( + <div className="buttonCollections"> + <button type="button" onClick={this.drawingButton}> + Drawing - Create diagram from ink drawing + </button> + <button type="button" onClick={this.gptButton}> + GPT - Generate diagram with AI prompt + </button> + <button type="button" onClick={this.mermaidButton}> + Mermaid Editor - Create diagram with mermaid code + </button> + </div> + ); + } + renderDrawing(): React.ReactNode { + return ( + <div ref={this._dragRef} className="DiagramBox-wrapper"> + <div className="content"> + <div className="topBar"> + <button className="backButtonDrawing" type="button" onClick={this.optionButton}> + Back + </button> + {!this.Document.mermaidCode && <p>Click the red pen icon to flip onto the collection side and draw a diagram with ink</p>} + </div> + <div id={'dashDiv' + this.Document.title} className="diagramBox" /> + </div> + </div> + ); + } - render() { + renderGpt(): React.ReactNode { return ( - <div ref={this._ref} className="DIYNodeBox"> - <div ref={this._dragRef} className="DIYNodeBox-wrapper"> + <div ref={this._dragRef} className="DiagramBox-wrapper"> + <div className="content"> <div className="search-bar"> - <input type="text" value={this.inputValue} onChange={this.handleInputChange} /> - <button type="button" onClick={this.handleRenderClick}> - Generate + <button className="backButton" type="button" onClick={this.optionButton}> + Back </button> + <textarea value={this.inputValue} placeholder="Enter GPT prompt" onChange={this.handleInputChange} onInput={e => this.autoResize(e.target as HTMLTextAreaElement)} /> + <div className="rightButtons"> + <button className="generateButton" type="button" onClick={this.handleRenderClick}> + Generate + </button> + <button className="convertButton" type="button" onClick={this.handleConvertButton}> + Edit + </button> + </div> </div> - <div className="content"> - {this.mermaidCode ? ( - <div id={'dashDiv' + this.Document.title} className="diagramBox" /> - ) : ( - <div>{this.loading ? <div className="loading-circle" /> : <div>{this.errorMessage ? this.errorMessage : 'Insert prompt to generate diagram'}</div>}</div> - )} + {this.mermaidCode ? ( + <div id={'dashDiv' + this.Document.title} className="diagramBox" /> + ) : ( + <div>{this.loading ? <div className="loading-circle" /> : <div>{this.errorMessage ? this.errorMessage : 'Insert prompt to generate diagram'}</div>}</div> + )} + </div> + </div> + ); + } + handleConvertButton = () => { + this.Document.menuState = 'mermaidCode'; + if (typeof this.Document.gptMermaidCode === 'string') { + this.createInputValue = this.removeFirstEmptyLine(this.Document.gptMermaidCode); + console.log(this.Document.gptMermaidCode); + this.renderMermaidAsync(this.Document.gptMermaidCode); + } + }; + removeFirstEmptyLine(input: string): string { + const lines = input.split('\n'); + let emptyLineRemoved = false; + const resultLines = lines.filter(line => { + if (!emptyLineRemoved && line.trim() === '') { + emptyLineRemoved = true; + return false; + } + return true; + }); + return resultLines.join('\n'); + } + + renderMermaidCode(): React.ReactNode { + return ( + <div ref={this._dragRef} className="DiagramBox-wrapper"> + <div className="contentCode"> + <div className="search-bar"> + <button className="backButton" type="button" onClick={this.optionButton}> + Back + </button> + <button className="exampleButton" type="button" onClick={this.exampleButton}> + Examples + </button> </div> + {this.isExampleMenuOpen && ( + <div className="exampleButtonContainer"> + <button type="button" onClick={this.flowButton}> + Flow + </button> + <button type="button" onClick={this.pieButton}> + Pie + </button> + <button type="button" onClick={this.timelineButton}> + Timeline + </button> + <button type="button" onClick={this.classButton}> + Class + </button> + <button type="button" onClick={this.mindmapButton}> + Mindmap + </button> + </div> + )} + <textarea value={this.createInputValue} placeholder="Enter Mermaid Code" onChange={this.handleInputChangeEditor} /> + <div id={'dashDiv' + this.Document.title} className="diagramBox" /> </div> </div> ); } + exampleButton = () => { + if (this.isExampleMenuOpen) { + this.isExampleMenuOpen = false; + } else { + this.isExampleMenuOpen = true; + } + }; + flowButton = () => { + this.createInputValue = `flowchart TD + A[Christmas] -->|Get money| B(Go shopping) + B --> C{Let me think} + C -->|One| D[Laptop] + C -->|Two| E[iPhone] + C -->|Three| F[fa:fa-car Car]`; + this.renderMermaidAsync(this.createInputValue); + }; + pieButton = () => { + this.createInputValue = `pie title Pets adopted by volunteers + "Dogs" : 386 + "Cats" : 85 + "Rats" : 15`; + this.renderMermaidAsync(this.createInputValue); + }; + timelineButton = () => { + this.createInputValue = `gantt + title A Gantt Diagram + dateFormat YYYY-MM-DD + section Section + A task :a1, 2014-01-01, 30d + Another task :after a1 , 20d + section Another + Task in sec :2014-01-12 , 12d + another task : 24d`; + this.renderMermaidAsync(this.createInputValue); + }; + classButton = () => { + this.createInputValue = `classDiagram + Animal <|-- Duck + Animal <|-- Fish + Animal <|-- Zebra + Animal : +int age + Animal : +String gender + Animal: +isMammal() + Animal: +mate() + class Duck{ + +String beakColor + +swim() + +quack() + } + class Fish{ + -int sizeInFeet + -canEat() + } + class Zebra{ + +bool is_wild + +run() + }`; + this.renderMermaidAsync(this.createInputValue); + }; + mindmapButton = () => { + this.createInputValue = `mindmap + root((mindmap)) + Origins + Long history + ::icon(fa fa-book) + Popularisation + British popular psychology author Tony Buzan + Research + On effectivness<br/>and features + On Automatic creation + Uses + Creative techniques + Strategic planning + Argument mapping + Tools + Pen and paper + Mermaid`; + this.renderMermaidAsync(this.createInputValue); + }; + handleInputChangeEditor = (e: React.ChangeEvent<HTMLTextAreaElement>) => { + if (typeof e.target.value === 'string') { + this.createInputValue = e.target.value; + this.renderMermaidAsync(e.target.value); + } + }; + removeWhitespace(str: string): string { + return str.replace(/\s+/g, ''); + } + autoResize(element: HTMLTextAreaElement): void { + element.style.height = '5px'; + element.style.height = element.scrollHeight + 'px'; + } + timeline = `gantt + title College Timeline + dateFormat YYYY-MM-DD + section Semester 1 + Orientation :done, des1, 2023-08-01, 2023-08-03 + Classes Start :active, des2, 2023-08-04, 2023-12-15 + Midterm Exams : des3, 2023-10-15, 2023-10-20 + End of Semester : des4, 2023-12-16, 2023-12-20 + section Semester 2 + Classes Start : des5, 2024-01-10, 2024-05-15 + Spring Break : des6, 2024-03-15, 2024-03-22 + Midterm Exams : des7, 2024-03-25, 2024-03-30 + Final Exams : des8, 2024-05-10, 2024-05-15 + section Summer Break + Internship : des9, 2024-06-01, 2024-08-31 + section Semester 3 + Classes Start : des10, 2024-09-01, 2025-12-15 + Midterm Exams : des11, 2024-11-15, 2024-11-20 + End of Semester : des12, 2025-12-16, 2025-12-20 + section Semester 4 + Classes Start : des13, 2025-01-10, 2025-05-15 + Spring Break : des14, 2025-03-15, 2025-03-22 + Midterm Exams : des15, 2025-03-25, 2025-03-30 + Final Exams : des16, 2025-05-10, 2025-05-15 + Graduation : des17, 2025-05-20, 2025-05-21`; + render() { + this.switchRenderDiv(); + return ( + <div ref={this._ref} className="DiagramBox"> + {this.renderDiv} + </div> + ); + } } Docs.Prototypes.TemplateMap.set(DocumentType.DIAGRAM, { layout: { view: DiagramBox, dataField: 'dadta' }, - options: { _height: 300, _layout_fitWidth: true, _layout_nativeDimEditable: true, _layout_reflowVertical: true, waitForDoubleClickToClick: 'always', systemIcon: 'BsGlobe' }, + options: { _height: 700, _width: 700, _layout_fitWidth: false, _layout_nativeDimEditable: true, _layout_reflowVertical: true, waitForDoubleClickToClick: 'always', _layout_reflowHorizontal: true, systemIcon: 'BsGlobe' }, }); diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index 3be50f5e6..e6590958b 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 ScreenshotBox 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 Box 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/client/views/smartdraw/AnnotationPalette.scss b/src/client/views/smartdraw/AnnotationPalette.scss new file mode 100644 index 000000000..9f875f61a --- /dev/null +++ b/src/client/views/smartdraw/AnnotationPalette.scss @@ -0,0 +1,10 @@ +.annotation-palette { + display: flex; + flex-direction: column; + align-items: center; + position: absolute; + right: 14px; + top: 50px; + border-radius: 5px; + margin: auto; +} diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx new file mode 100644 index 000000000..ec4279e3e --- /dev/null +++ b/src/client/views/smartdraw/AnnotationPalette.tsx @@ -0,0 +1,382 @@ +import { faLaptopHouse } from '@fortawesome/free-solid-svg-icons'; +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Slider, Switch } from '@mui/material'; +import { Button, IconButton } from 'browndash-components'; +import { action, computed, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { AiOutlineSend } from 'react-icons/ai'; +import ReactLoading from 'react-loading'; +import { returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils'; +import { ActiveInkWidth, Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc'; +import { DocData } from '../../../fields/DocSymbols'; +import { InkData, InkField } from '../../../fields/InkField'; +import { BoolCast, DocCast, ImageCast } from '../../../fields/Types'; +import { emptyFunction, unimplementedFunction } from '../../../Utils'; +import { Docs } from '../../documents/Documents'; +import { makeUserTemplateButton, makeUserTemplateImage } from '../../util/DropConverter'; +import { SettingsManager } from '../../util/SettingsManager'; +import { Transform } from '../../util/Transform'; +import { undoable, undoBatch } from '../../util/UndoManager'; +import { CollectionFreeFormView, MarqueeOptionsMenu, MarqueeView } from '../collections/collectionFreeForm'; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveIsInkMask, DocumentView, DocumentViewInternal } from '../nodes/DocumentView'; +import { FieldView } from '../nodes/FieldView'; +import { ObservableReactComponent } from '../ObservableReactComponent'; +import { DefaultStyleProvider } from '../StyleProvider'; +import './AnnotationPalette.scss'; +import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler'; +import { DocumentType } from '../../documents/DocumentTypes'; +import { ImageField } from '../../../fields/URLField'; +import { CollectionCarousel3DView } from '../collections/CollectionCarousel3DView'; +import { Copy } from '../../../fields/FieldSymbols'; + +interface AnnotationPaletteProps { + Document: Doc; +} + +@observer +export class AnnotationPalette extends ObservableReactComponent<AnnotationPaletteProps> { + @observable private _paletteMode: 'create' | 'view' = 'view'; + @observable private _userInput: string = ''; + @observable private _isLoading: boolean = false; + @observable private _canInteract: boolean = true; + @observable private _showRegenerate: boolean = false; + @observable private _docView: DocumentView | null = null; + @observable private _docCarouselView: DocumentView | null = null; + @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 }; + private _gptRes: string[] = []; + + constructor(props: any) { + super(props); + makeObservable(this); + } + + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(AnnotationPalette, fieldKey); + } + + Contains = (view: DocumentView) => { + return (this._docView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docView)) || (this._docCarouselView && (view.containerViewPath?.() ?? []).concat(view).includes(this._docCarouselView)); + }; + + return170 = () => 170; + + @action + handleKeyPress = async (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + await this.generateDrawing(); + } + }; + + @action + setPaletteMode = (mode: 'create' | 'view') => { + this._paletteMode = mode; + }; + + @action + setUserInput = (input: string) => { + if (!this._isLoading) this._userInput = input; + }; + + @action + setDetail = (detail: number) => { + if (this._canInteract) this._opts.complexity = detail; + }; + + @action + setColor = (autoColor: boolean) => { + if (this._canInteract) this._opts.autoColor = autoColor; + }; + + @action + setSize = (size: number) => { + if (this._canInteract) this._opts.size = size; + }; + + @action + resetPalette = (changePaletteMode: boolean) => { + if (changePaletteMode) this.setPaletteMode('view'); + this.setUserInput(''); + this.setDetail(5); + this.setColor(true); + this.setSize(200); + this._showRegenerate = false; + this._canInteract = true; + this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 }; + this._gptRes = []; + this._props.Document[DocData].data = undefined; + }; + + public static addToPalette = async (doc: Doc) => { + if (!doc.savedAsAnno) { + const clone = await Doc.MakeClone(doc); + clone.clone.title = doc.title; + const image = (await AnnotationPalette.getIcon(doc))?.[Copy](); + if (image) { + const imageTemplate = makeUserTemplateImage(clone.clone, image); + Doc.AddDocToList(Doc.MyAnnos, 'data', imageTemplate); + doc.savedAsAnno = true; + } + } + }; + + public static getIcon(group: Doc) { + const docView = DocumentView.getDocumentView(group); + if (docView) { + docView.ComponentView?.updateIcon?.(true); + return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000)); + } + return undefined; + } + + @undoBatch + generateDrawing = action(async () => { + this._isLoading = true; + this._props.Document[DocData].data = undefined; + for (var i = 0; i < 3; i++) { + try { + SmartDrawHandler.Instance._addFunc = this.createDrawing; + 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); + } + } catch (e) { + console.log('Error generating drawing'); + } + } + this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput); + this._userInput = ''; + this._isLoading = false; + this._showRegenerate = true; + }); + + @action + createDrawing = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => { + this._opts = opts; + 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); + } + }; + + saveDrawing = async () => { + const cIndex: number = this._props.Document.carousel_index as number; + const focusedDrawing = DocListCast(this._props.Document.data)[cIndex]; + const docData = focusedDrawing[DocData]; + docData.title = this._opts.text.match(/^(.*?)~~~.*$/)?.[1] || this._opts.text; + docData.drawingInput = this._opts.text; + docData.drawingComplexity = this._opts.complexity; + docData.drawingColored = this._opts.autoColor; + docData.drawingSize = this._opts.size; + docData.drawingData = this._gptRes[cIndex]; + docData.width = this._opts.size; + await AnnotationPalette.addToPalette(focusedDrawing); + this.resetPalette(true); + }; + + async getIcon(group: Doc) { + const docView = DocumentView.getDocumentView(group); + if (docView) { + docView.ComponentView?.updateIcon?.(true); + return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000)); + } + return undefined; + } + + render() { + return ( + <div className="annotation-palette" style={{ zIndex: 1000 }} onClick={e => e.stopPropagation()}> + {this._paletteMode === 'view' && ( + <> + <DocumentView + ref={r => (this._docView = r)} + Document={Doc.MyAnnos} + addDocument={undefined} + addDocTab={DocumentViewInternal.addDocTabFunc} + pinToPres={DocumentView.PinDoc} + containerViewPath={returnEmptyDoclist} + styleProvider={DefaultStyleProvider} + removeDocument={returnFalse} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={this.return170} + PanelHeight={this.return170} + renderDepth={0} + isContentActive={returnTrue} + focus={emptyFunction} + whenChildContentsActiveChanged={emptyFunction} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + /> + <Button text="Add" icon={<FontAwesomeIcon icon="square-plus" />} color={SettingsManager.userColor} onClick={() => this.setPaletteMode('create')} /> + </> + )} + {this._paletteMode === 'create' && ( + <> + <div style={{ display: 'flex', flexDirection: 'row', width: '170px' }}> + <input + aria-label="label-input" + id="new-label" + type="text" + style={{ color: 'black', width: '170px' }} + value={this._userInput} + onChange={e => { + this.setUserInput(e.target.value); + }} + placeholder={this._showRegenerate ? '(Optional) Enter edits' : 'Enter item to draw'} + onKeyDown={this.handleKeyPress} + /> + <Button + style={{ alignSelf: 'flex-end' }} + tooltip={this._showRegenerate ? 'Regenerate' : 'Send'} + 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} + /> + </div> + <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', width: '170px', marginTop: '5px' }}> + <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '40px' }}> + Color + <Switch + sx={{ + '& .MuiSwitch-switchBase.Mui-checked': { + color: SettingsManager.userColor, + }, + '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { + backgroundColor: SettingsManager.userVariantColor, + }, + }} + defaultChecked={true} + value={this._opts.autoColor} + size="small" + onChange={() => this.setColor(!this._opts.autoColor)} + /> + </div> + <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '60px' }}> + Detail + <Slider + sx={{ + '& .MuiSlider-thumb': { + color: SettingsManager.userColor, + '&.Mui-focusVisible, &:hover, &.Mui-active': { + boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`, + }, + }, + '& .MuiSlider-track': { + color: SettingsManager.userVariantColor, + }, + '& .MuiSlider-rail': { + color: SettingsManager.userColor, + }, + }} + style={{ width: '80%' }} + min={1} + max={10} + step={1} + size="small" + value={this._opts.complexity} + onChange={(e, val) => { + this.setDetail(val as number); + }} + valueLabelDisplay="auto" + /> + </div> + <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '60px' }}> + Size + <Slider + sx={{ + '& .MuiSlider-thumb': { + color: SettingsManager.userColor, + '&.Mui-focusVisible, &:hover, &.Mui-active': { + boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`, + }, + }, + '& .MuiSlider-track': { + color: SettingsManager.userVariantColor, + }, + '& .MuiSlider-rail': { + color: SettingsManager.userColor, + }, + }} + style={{ width: '80%' }} + min={50} + max={500} + step={10} + size="small" + value={this._opts.size} + onChange={(e, val) => { + this.setSize(val as number); + }} + valueLabelDisplay="auto" + /> + </div> + </div> + <DocumentView + ref={r => (this._docCarouselView = r)} + Document={this._props.Document} + addDocument={undefined} + addDocTab={DocumentViewInternal.addDocTabFunc} + pinToPres={DocumentView.PinDoc} + containerViewPath={returnEmptyDoclist} + styleProvider={DefaultStyleProvider} + removeDocument={returnFalse} + ScreenToLocalTransform={Transform.Identity} + PanelWidth={this.return170} + PanelHeight={this.return170} + renderDepth={1} + isContentActive={returnTrue} + focus={emptyFunction} + whenChildContentsActiveChanged={emptyFunction} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + searchFilterDocs={returnEmptyDoclist} + /> + <div style={{ width: '100%', display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}> + <Button text="Back" tooltip="Back to All Annotations" icon={<FontAwesomeIcon icon="reply" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(true)} /> + <div style={{ display: 'flex', flexDirection: 'row' }}> + <Button tooltip="Save" icon={<FontAwesomeIcon icon="file-arrow-down" />} color={SettingsManager.userColor} onClick={this.saveDrawing} /> + <Button tooltip="Reset" icon={<FontAwesomeIcon icon="rotate-left" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(false)} /> + </div> + </div> + </> + )} + </div> + ); + } +} + +Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, { + layout: { view: AnnotationPalette, dataField: 'data' }, + options: { acl: '' }, +}); diff --git a/src/client/views/smartdraw/SmartDrawHandler.scss b/src/client/views/smartdraw/SmartDrawHandler.scss new file mode 100644 index 000000000..6d402a80f --- /dev/null +++ b/src/client/views/smartdraw/SmartDrawHandler.scss @@ -0,0 +1,3 @@ +.smart-draw-handler { + position: absolute; +} diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx new file mode 100644 index 000000000..c842551c3 --- /dev/null +++ b/src/client/views/smartdraw/SmartDrawHandler.tsx @@ -0,0 +1,437 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { action, makeObservable, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import React from 'react'; +import { SettingsManager } from '../../util/SettingsManager'; +import { ObservableReactComponent } from '../ObservableReactComponent'; +import { Button, IconButton } 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 { SVGToBezier } from '../../util/bezierFit'; +const { parse } = require('svgson'); +import { Slider, Switch } from '@mui/material'; +import { Doc } from '../../../fields/Doc'; +import { DocData } from '../../../fields/DocSymbols'; +import { DocumentView } from '../nodes/DocumentView'; +import { BoolCast, NumCast, StrCast } from '../../../fields/Types'; +import './SmartDrawHandler.scss'; +import { unimplementedFunction } from '../../../Utils'; + +export interface DrawingOptions { + text: string; + complexity: number; + size: number; + autoColor: boolean; + x: number; + y: number; +} + +@observer +export class SmartDrawHandler extends ObservableReactComponent<{}> { + static Instance: SmartDrawHandler; + + @observable private _display: boolean = false; + @observable private _pageX: number = 0; + @observable private _pageY: number = 0; + @observable private _yRelativeToTop: boolean = true; + @observable private _isLoading: boolean = false; + @observable private _userInput: string = ''; + @observable private _showOptions: boolean = false; + @observable private _showEditBox: boolean = false; + @observable public _showRegenerate: boolean = false; + @observable private _complexity: number = 5; + @observable private _size: number = 200; + @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; + + constructor(props: any) { + super(props); + makeObservable(this); + SmartDrawHandler.Instance = this; + } + + @action + setUserInput = (input: string) => { + if (this._canInteract) this._userInput = input; + }; + + @action + setRegenInput = (input: string) => { + if (this._canInteract) this._regenInput = input; + }; + + @action + setShowOptions = () => { + this._showOptions = !this._showOptions; + }; + + @action + setComplexity = (val: number) => { + if (this._canInteract) this._complexity = val; + }; + + @action + setSize = (val: number) => { + if (this._canInteract) this._size = val; + }; + + @action + setAutoColor = () => { + if (this._canInteract) this._autoColor = !this._autoColor; + }; + + @action + displaySmartDrawHandler = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => { + 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) => { + 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; + 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 }; + }; + + @action + hideSmartDrawHandler = () => { + this._showRegenerate = false; + this._display = false; + this._isLoading = false; + this._showOptions = false; + this._userInput = ''; + this._complexity = 5; + this._size = 350; + this._autoColor = true; + Doc.ActiveTool = InkTool.None; + }; + + @action + hideRegenerate = () => { + if (!this._isLoading) { + this._showRegenerate = false; + this._isLoading = false; + this._regenInput = ''; + this._lastInput = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 }; + } + }; + + @action + handleKeyPress = async (event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + await this.handleSendClick(); + } + }; + + @action + handleSendClick = async () => { + this._isLoading = true; + this._canInteract = false; + if (this._showRegenerate) { + await this.regenerate(); + this._regenInput = ''; + this._showEditBox = false; + } else { + 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(); + } catch (err) { + if (this._errorOccurredOnce) { + console.error('GPT call failed', err); + this._errorOccurredOnce = false; + } else { + this._errorOccurredOnce = true; + await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor); + } + } + } + this._isLoading = false; + this._canInteract = true; + }; + + @action + drawWithGPT = async (startPt: { X: number; Y: number }, input: string, complexity: number, size: number, autoColor: boolean) => { + if (input === '') return; + this._lastInput = { text: input, complexity: complexity, size: size, autoColor: autoColor, x: startPt.X, y: startPt.Y }; + const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true); + if (!res) { + console.error('GPT call failed'); + return; + } + console.log(res); + const strokeData = await this.parseResponse(res, startPt, false, autoColor); + 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; + if (regenInput) this._regenInput = regenInput; + + try { + let res; + if (this._regenInput !== '') { + const prompt: string = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`; + res = await gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true); + this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`; + } else { + res = await gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true); + } + if (!res) { + console.error('GPT call failed'); + return; + } + console.log(res); + await this.parseResponse(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor); + } catch (err) { + console.error('GPT call failed', err); + } + }; + + @action + parseResponse = 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]; + const svgObject = await parse(svg[0]); + const svgStrokes: any = svgObject.children; + const strokeData: [InkData, string, string][] = []; + svgStrokes.forEach((child: any) => { + const convertedBezier: InkData = SVGToBezier(child.name, child.attributes); + strokeData.push([ + convertedBezier.map(point => { + return { X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 }; + }), + (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : undefined, + (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] }; + } + }; + + render() { + if (this._display) { + return ( + <div + id="label-handler" + className="smart-draw-handler" + style={{ + display: this._display ? '' : 'none', + left: this._pageX, + ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }), + background: SettingsManager.userBackgroundColor, + color: SettingsManager.userColor, + }}> + <div> + <IconButton + tooltip={'Cancel'} + onClick={() => { + this.hideSmartDrawHandler(); + this.hideRegenerate(); + }} + icon={<FontAwesomeIcon icon="xmark" />} + color={SettingsManager.userColor} + style={{ width: '19px' }} + /> + <input + aria-label="Smart Draw Input" + className="smartdraw-input" + id="smartdraw-input" + type="text" + style={{ color: 'black' }} + value={this._userInput} + onChange={e => { + this.setUserInput(e.target.value); + }} + placeholder="Enter item to draw" + onKeyDown={this.handleKeyPress} + /> + <IconButton tooltip="Advanced Options" icon={<FontAwesomeIcon icon={this._showOptions ? 'caret-down' : 'caret-right'} />} color={SettingsManager.userColor} style={{ width: '14px' }} onClick={this.setShowOptions} /> + <Button + style={{ alignSelf: 'flex-end' }} + text="Send" + icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} + iconPlacement="right" + color={SettingsManager.userColor} + onClick={this.handleSendClick} + /> + </div> + {this._showOptions && ( + <> + <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around' }}> + <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '30%' }}> + Auto color + <Switch + sx={{ + '& .MuiSwitch-switchBase.Mui-checked': { + color: SettingsManager.userColor, + }, + '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': { + backgroundColor: SettingsManager.userVariantColor, + }, + }} + defaultChecked={true} + value={this._autoColor} + size="small" + onChange={this.setAutoColor} + /> + </div> + <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '31%' }}> + Complexity + <Slider + sx={{ + '& .MuiSlider-thumb': { + color: SettingsManager.userColor, + '&.Mui-focusVisible, &:hover, &.Mui-active': { + boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`, + }, + }, + '& .MuiSlider-track': { + color: SettingsManager.userVariantColor, + }, + '& .MuiSlider-rail': { + color: SettingsManager.userColor, + }, + }} + style={{ width: '80%' }} + min={1} + max={10} + step={1} + size="small" + value={this._complexity} + onChange={(e, val) => { + this.setComplexity(val as number); + }} + valueLabelDisplay="auto" + /> + </div> + <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '39%' }}> + Size (in pixels) + <Slider + sx={{ + '& .MuiSlider-thumb': { + color: SettingsManager.userColor, + '&.Mui-focusVisible, &:hover, &.Mui-active': { + boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`, + }, + }, + '& .MuiSlider-track': { + color: SettingsManager.userVariantColor, + }, + '& .MuiSlider-rail': { + color: SettingsManager.userColor, + }, + }} + style={{ width: '80%' }} + min={50} + max={700} + step={10} + size="small" + value={this._size} + onChange={(e, val) => { + this.setSize(val as number); + }} + valueLabelDisplay="auto" + /> + </div> + </div> + </> + )} + </div> + ); + } else if (this._showRegenerate) { + return ( + <div + id="smartdraw-options-menu" + className="smart-draw-handler" + style={{ + left: this._pageX, + ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }), + background: SettingsManager.userBackgroundColor, + color: SettingsManager.userColor, + }}> + <div + style={{ + display: 'flex', + flexDirection: 'row', + }}> + <IconButton + tooltip="Regenerate" + icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />} + color={SettingsManager.userColor} + onClick={this.handleSendClick} + /> + <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.edit} /> + {this._showEditBox && ( + <div + style={{ + display: 'flex', + flexDirection: 'row', + }}> + <input + aria-label="Edit instructions input" + className="smartdraw-input" + id="regen-input" + type="text" + style={{ color: 'black' }} + value={this._regenInput} + onChange={e => { + this.setRegenInput(e.target.value); + }} + onKeyDown={this.handleKeyPress} + placeholder="Edit instructions" + /> + <Button + style={{ alignSelf: 'flex-end' }} + text="Send" + icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />} + iconPlacement="right" + color={SettingsManager.userColor} + onClick={this.handleSendClick} + /> + </div> + )} + </div> + </div> + ); + } else { + return <></>; + } + } +} diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 2792f3aba..e9edb8363 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -231,6 +231,8 @@ export class Doc extends RefField { public static get MyPublishedDocs() { return DocListCast(Doc.ActiveDashboard?.myPublishedDocs).concat(DocListCast(DocCast(Doc.UserDoc().myPublishedDocs)?.data)); } // prettier-ignore public static get MyDashboards() { return DocCast(Doc.UserDoc().myDashboards); } // prettier-ignore public static get MyTemplates() { return DocCast(Doc.UserDoc().myTemplates); } // prettier-ignore + public static get MyAnnos() { return DocCast(Doc.UserDoc().myAnnos); } // prettier-ignore + public static get MyLightboxDrawings() { return DocCast(Doc.UserDoc().myLightboxDrawings); } // prettier-ignore public static get MyImports() { return DocCast(Doc.UserDoc().myImports); } // prettier-ignore public static get MyFilesystem() { return DocCast(Doc.UserDoc().myFilesystem); } // prettier-ignore public static get MyTools() { return DocCast(Doc.UserDoc().myTools); } // prettier-ignore @@ -1695,8 +1697,8 @@ ScriptingGlobals.add(function getEmbedding(doc: any) { return Doc.MakeEmbedding(doc); }); // eslint-disable-next-line prefer-arrow-callback -ScriptingGlobals.add(function getCopy(doc: any, copyProto: any) { - return doc.isTemplateDoc ? Doc.MakeDelegateWithProto(doc) : Doc.MakeCopy(doc, copyProto); +ScriptingGlobals.add(async function getCopy(doc: any, copyProto: any) { + return doc.isTemplateDoc ? (doc.isGroup ? (await Doc.MakeClone(doc)).clone : Doc.MakeDelegateWithProto(doc)) : Doc.MakeCopy(doc, copyProto); }); // eslint-disable-next-line prefer-arrow-callback ScriptingGlobals.add(function copyField(field: any) { diff --git a/src/pen-gestures/GestureTypes.ts b/src/pen-gestures/GestureTypes.ts index d86562580..2e1c9d16f 100644 --- a/src/pen-gestures/GestureTypes.ts +++ b/src/pen-gestures/GestureTypes.ts @@ -7,6 +7,7 @@ export enum Gestures { Circle = 'circle', Rectangle = 'rectangle', Arrow = 'arrow', + RightAngle = 'rightangle', } // Defines a point in an ink as a pair of x- and y-coordinates. diff --git a/src/pen-gestures/ndollar.ts b/src/pen-gestures/ndollar.ts index ff7f7310b..31a4eb0e8 100644 --- a/src/pen-gestures/ndollar.ts +++ b/src/pen-gestures/ndollar.ts @@ -209,6 +209,7 @@ export class NDollarRecognizer { ]) ) ); + this.Multistrokes.push(new Multistroke(Gestures.Rectangle, useBoundedRotationInvariance, new Array([new Point(30, 143), new Point(106, 146), new Point(106, 225), new Point(30, 222), new Point(30, 146)]))); this.Multistrokes.push(new Multistroke(Gestures.Line, useBoundedRotationInvariance, [[new Point(12, 347), new Point(119, 347)]])); this.Multistrokes.push( new Multistroke( @@ -219,6 +220,13 @@ export class NDollarRecognizer { ); this.Multistrokes.push( new Multistroke( + Gestures.Triangle, // equilateral + useBoundedRotationInvariance, + new Array([new Point(42, 100), new Point(140, 102), new Point(100, 200), new Point(40, 100)]) + ) + ); + this.Multistrokes.push( + new Multistroke( Gestures.Circle, useBoundedRotationInvariance, new Array([ @@ -236,6 +244,94 @@ export class NDollarRecognizer { ]) ) ); + this.Multistrokes.push( + new Multistroke( + Gestures.Circle, + useBoundedRotationInvariance, + new Array([ + new Point(201, 250), + new Point(160, 230), + new Point(151, 210), + new Point(151, 190), + new Point(160, 170), + new Point(200, 150), + new Point(240, 170), + new Point(248, 190), + new Point(248, 210), + new Point(240, 230), + new Point(200, 250), + ]) + ) + ); + 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)]))); NumMultistrokes = this.Multistrokes.length; // NumMultistrokes flags the end of the non user-defined gstures strokes // // PREDEFINED STROKES |