diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/.DS_Store | bin | 10244 -> 14340 bytes | |||
-rw-r--r-- | src/client/apis/gpt/GPT.ts | 32 | ||||
-rw-r--r-- | src/client/cognitive_services/CognitiveServices.ts | 11 | ||||
-rw-r--r-- | src/client/documents/Documents.ts | 2 | ||||
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 2 | ||||
-rw-r--r-- | src/client/views/DocumentDecorations.tsx | 2 | ||||
-rw-r--r-- | src/client/views/GestureOverlay.tsx | 8 | ||||
-rw-r--r-- | src/client/views/InkTranscription.scss | 5 | ||||
-rw-r--r-- | src/client/views/InkTranscription.tsx | 765 | ||||
-rw-r--r-- | src/client/views/InkingStroke.tsx | 4 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 2 | ||||
-rw-r--r-- | src/client/views/global/globalScripts.ts | 6 | ||||
-rw-r--r-- | src/client/views/nodes/DiagramBox.scss | 250 | ||||
-rw-r--r-- | src/client/views/nodes/DiagramBox.tsx | 619 | ||||
-rw-r--r-- | src/client/views/nodes/ScreenshotBox.tsx | 2 | ||||
-rw-r--r-- | src/pen-gestures/GestureTypes.ts | 1 | ||||
-rw-r--r-- | src/pen-gestures/ndollar.ts | 96 |
17 files changed, 1289 insertions, 518 deletions
diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex d7a0cd9d4..19eea5fce 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 a7bd05a21..cbe2f0582 100644 --- a/src/client/apis/gpt/GPT.ts +++ b/src/client/apis/gpt/GPT.ts @@ -156,5 +156,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/Documents.ts b/src/client/documents/Documents.ts index b96fdb4bd..ff95e38bd 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); } diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index fa0cb920d..98a54d4d0 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -425,7 +425,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)}, diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index 93c3e3338..a229b15db 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -872,4 +872,4 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora </div> ); } -} +}
\ No newline at end of file diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index a1f7712c1..649208989 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/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/MainView.tsx b/src/client/views/MainView.tsx index d9a8730e4..fd1af7547 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -78,6 +78,7 @@ 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; @@ -1112,6 +1113,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/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index d46e72d75..bda3d0ebb 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/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 |