aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/.DS_Storebin10244 -> 10244 bytes
-rw-r--r--src/ClientUtils.ts7
-rw-r--r--src/Utils.ts3
-rw-r--r--src/client/apis/gpt/GPT.ts84
-rw-r--r--src/client/apis/youtube/.YoutubeBox.scss.icloudbin165 -> 0 bytes
-rw-r--r--src/client/apis/youtube/.YoutubeBox.tsx.icloudbin162 -> 0 bytes
-rw-r--r--src/client/cognitive_services/CognitiveServices.ts2
-rw-r--r--src/client/documents/DocUtils.ts14
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/documents/Documents.ts15
-rw-r--r--src/client/util/CurrentUserUtils.ts67
-rw-r--r--src/client/util/DropConverter.ts18
-rw-r--r--src/client/util/Scripting.ts7
-rw-r--r--src/client/util/SettingsManager.tsx2
-rw-r--r--src/client/util/bezierFit.ts160
-rw-r--r--src/client/views/ContextMenu.tsx5
-rw-r--r--src/client/views/ContextMenuItem.tsx2
-rw-r--r--src/client/views/DashboardView.tsx8
-rw-r--r--src/client/views/DocumentDecorations.tsx17
-rw-r--r--src/client/views/GestureOverlay.tsx450
-rw-r--r--src/client/views/GlobalKeyHandler.ts2
-rw-r--r--src/client/views/InkStrokeProperties.ts180
-rw-r--r--src/client/views/InkTranscription.scss5
-rw-r--r--src/client/views/InkTranscription.tsx760
-rw-r--r--src/client/views/InkingStroke.tsx25
-rw-r--r--src/client/views/LightboxView.scss24
-rw-r--r--src/client/views/LightboxView.tsx26
-rw-r--r--src/client/views/Main.tsx13
-rw-r--r--src/client/views/MainView.tsx36
-rw-r--r--src/client/views/MarqueeAnnotator.tsx8
-rw-r--r--src/client/views/PropertiesView.scss6
-rw-r--r--src/client/views/PropertiesView.tsx204
-rw-r--r--src/client/views/TagsView.tsx10
-rw-r--r--src/client/views/ViewBoxInterface.ts2
-rw-r--r--src/client/views/collections/CollectionCardDeckView.tsx10
-rw-r--r--src/client/views/collections/CollectionCarousel3DView.tsx5
-rw-r--r--src/client/views/collections/CollectionCarouselView.tsx16
-rw-r--r--src/client/views/collections/CollectionDockingView.tsx9
-rw-r--r--src/client/views/collections/CollectionMenu.scss2
-rw-r--r--src/client/views/collections/TreeView.tsx2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx388
-rw-r--r--src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss14
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx3
-rw-r--r--src/client/views/collections/collectionFreeForm/MarqueeView.tsx43
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaView.tsx1
-rw-r--r--src/client/views/global/globalScripts.ts93
-rw-r--r--src/client/views/linking/.LinkRelationshipSearch.tsx.icloudbin176 -> 0 bytes
-rw-r--r--src/client/views/nodes/.FaceRectangles.tsx.icloudbin168 -> 0 bytes
-rw-r--r--src/client/views/nodes/.LinkAnchorBox.tsx.icloudbin167 -> 0 bytes
-rw-r--r--src/client/views/nodes/AudioBox.tsx3
-rw-r--r--src/client/views/nodes/ComparisonBox.tsx190
-rw-r--r--src/client/views/nodes/DataVizBox/components/TableBox.tsx1
-rw-r--r--src/client/views/nodes/DiagramBox.scss4
-rw-r--r--src/client/views/nodes/DiagramBox.tsx19
-rw-r--r--src/client/views/nodes/DocumentView.tsx21
-rw-r--r--src/client/views/nodes/FontIconBox/FontIconBox.tsx8
-rw-r--r--src/client/views/nodes/ImageBox.tsx26
-rw-r--r--src/client/views/nodes/LabelBigText.js270
-rw-r--r--src/client/views/nodes/LabelBox.tsx2
-rw-r--r--src/client/views/nodes/PDFBox.tsx47
-rw-r--r--src/client/views/nodes/VideoBox.tsx15
-rw-r--r--src/client/views/nodes/WebBox.tsx30
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx40
-rw-r--r--src/client/views/nodes/formattedText/RichTextMenu.tsx11
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFill.tsx104
-rw-r--r--src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx34
-rw-r--r--src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts16
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx51
-rw-r--r--src/client/views/pdf/PDFViewer.tsx11
-rw-r--r--src/client/views/smartdraw/AnnotationPalette.scss56
-rw-r--r--src/client/views/smartdraw/AnnotationPalette.tsx361
-rw-r--r--src/client/views/smartdraw/SmartDrawHandler.scss44
-rw-r--r--src/client/views/smartdraw/SmartDrawHandler.tsx491
-rw-r--r--src/fields/Doc.ts7
-rw-r--r--src/fields/InkField.ts22
-rw-r--r--src/fields/SchemaHeaderField.ts1
-rw-r--r--src/pen-gestures/GestureTypes.ts2
-rw-r--r--src/pen-gestures/ndollar.ts28
-rw-r--r--src/typings/index.d.ts2
79 files changed, 3129 insertions, 1537 deletions
diff --git a/src/.DS_Store b/src/.DS_Store
index 426a2ee90..9b66f8d8e 100644
--- a/src/.DS_Store
+++ b/src/.DS_Store
Binary files differ
diff --git a/src/ClientUtils.ts b/src/ClientUtils.ts
index 01eda7e98..972910071 100644
--- a/src/ClientUtils.ts
+++ b/src/ClientUtils.ts
@@ -119,7 +119,6 @@ export namespace ClientUtils {
}
export function readUploadedFileAsText(inputFile: File) {
- // eslint-disable-next-line no-undef
const temporaryFileReader = new FileReader();
return new Promise((resolve, reject) => {
@@ -163,7 +162,7 @@ export namespace ClientUtils {
export function GetScreenTransform(ele?: HTMLElement | null): { scale: number; translateX: number; translateY: number } {
if (!ele) {
- return { scale: 1, translateX: 1, translateY: 1 };
+ return { scale: 0, translateX: 1, translateY: 1 };
}
const rect = ele.getBoundingClientRect();
const scale = ele.offsetWidth === 0 && rect.width === 0 ? 1 : rect.width / ele.offsetWidth;
@@ -336,7 +335,7 @@ export namespace ClientUtils {
try {
document.execCommand('paste');
- } catch (err) {
+ } catch {
/* empty */
}
@@ -575,9 +574,7 @@ export function setupMoveUpEvents(
moveEvent: (e: PointerEvent, down: number[], delta: number[]) => boolean,
upEvent: (e: PointerEvent, movement: number[], isClick: boolean) => void,
clickEvent: (e: PointerEvent, doubleTap?: boolean) => unknown,
- // eslint-disable-next-line default-param-last
stopPropagation: boolean = true,
- // eslint-disable-next-line default-param-last
stopMovePropagation: boolean = true,
noDoubleTapTimeout?: () => void
) {
diff --git a/src/Utils.ts b/src/Utils.ts
index 0590c6930..724725c23 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -1,4 +1,3 @@
-/* eslint-disable @typescript-eslint/no-namespace */
import * as uuid from 'uuid';
export function clamp(n: number, lower: number, upper: number) {
@@ -205,7 +204,6 @@ export function intersectRect(r1: { left: number; top: number; width: number; he
}
export function stringHash(s?: string) {
- // eslint-disable-next-line no-bitwise
return !s ? undefined : Math.abs(s.split('').reduce((a, b) => (n => n & n)((a << 5) - a + b.charCodeAt(0)), 0));
}
@@ -254,6 +252,7 @@ export namespace JSONUtils {
try {
results = JSON.parse(source);
} catch (e) {
+ console.log('JSONparse error: ', e);
results = source;
}
return results;
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 8b101bfe3..88352110b 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -14,6 +14,8 @@ enum GPTCallType {
DATA = 'data',
STACK = 'stack',
PRONUNCIATION = 'pronunciation',
+ DRAW = 'draw',
+ COLOR = 'color',
RUBRIC = 'rubric',
TYPE = 'type',
SUBSET = 'subset',
@@ -101,6 +103,18 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
temp: 0,
prompt: "Answer the user's question with a short (<100 word) response. If a particular document is selected I will provide that information (which may help with your response)",
},
+ draw: {
+ model: 'gpt-4o',
+ maxTokens: 1024,
+ temp: 0.8,
+ 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.',
+ },
+ color: {
+ model: 'gpt-4o',
+ maxTokens: 1024,
+ temp: 0.5,
+ prompt: 'You will be coloring drawings. You will be given what the drawing is, then a list of descriptions for parts of the drawing. Based on each description, respond with the stroke and fill color that it should be. Follow the rules: 1. Avoid using black for stroke color 2. Make the stroke color 1-3 shades darker than the fill color 3. Use the same colors when possible. Format as {#abcdef #abcdef}, making sure theres a color for each description, and do not include any additional text.',
+ },
};
let lastCall = '';
@@ -111,10 +125,10 @@ let lastResp = '';
* @param inputText Text to process
* @returns AI Output
*/
-const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: any) => {
+const gptAPICall = async (inputTextIn: string, callType: GPTCallType, prompt?: any, dontCache?: boolean) => {
const inputText = [GPTCallType.SUMMARY, GPTCallType.FLASHCARD, GPTCallType.QUIZ, GPTCallType.STACK].includes(callType) ? inputTextIn + '.' : inputTextIn;
const opts: GPTCallOpts = callTypeMap[callType];
- if (lastCall === inputText) return lastResp;
+ if (lastCall === inputText && dontCache !== true) return lastResp;
try {
lastCall = inputText;
@@ -199,5 +213,69 @@ const gptImageLabel = async (src: string, prompt: 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';
+ }
+};
+
+const gptDrawingColor = async (image: string, coords: string[]): Promise<string> => {
+ try {
+ const response = await openai.chat.completions.create({
+ model: 'gpt-4o',
+ temperature: 0,
+ messages: [
+ {
+ role: 'user',
+ content: [
+ {
+ type: 'text',
+ text: `Identify what the drawing in the image represents in 1-5 words. Then, given a list of a list of coordinates, where each list is the coordinates for one stroke of the drawing, determine which part of the drawing it is. Return just what the item it is, followed by ~~~ then only your descriptions in a list like [description, description, ...]. Here are the coordinates: ${coords}`,
+ },
+ {
+ type: 'image_url',
+ image_url: {
+ url: `${image}`,
+ 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, gptDrawingColor };
diff --git a/src/client/apis/youtube/.YoutubeBox.scss.icloud b/src/client/apis/youtube/.YoutubeBox.scss.icloud
deleted file mode 100644
index 44b9557bd..000000000
--- a/src/client/apis/youtube/.YoutubeBox.scss.icloud
+++ /dev/null
Binary files differ
diff --git a/src/client/apis/youtube/.YoutubeBox.tsx.icloud b/src/client/apis/youtube/.YoutubeBox.tsx.icloud
deleted file mode 100644
index bb5d12d7c..000000000
--- a/src/client/apis/youtube/.YoutubeBox.tsx.icloud
+++ /dev/null
Binary files differ
diff --git a/src/client/cognitive_services/CognitiveServices.ts b/src/client/cognitive_services/CognitiveServices.ts
index 9808b6a01..3ee61cbfb 100644
--- a/src/client/cognitive_services/CognitiveServices.ts
+++ b/src/client/cognitive_services/CognitiveServices.ts
@@ -336,7 +336,7 @@ export namespace CognitiveServices {
'Ocp-Apim-Subscription-Key': apiKey,
},
};
- return request.post(options);
+ return rp.post(options);
},
};
diff --git a/src/client/documents/DocUtils.ts b/src/client/documents/DocUtils.ts
index 1696de7ad..acaefe783 100644
--- a/src/client/documents/DocUtils.ts
+++ b/src/client/documents/DocUtils.ts
@@ -34,11 +34,8 @@ import { OpenWhere } from '../views/nodes/OpenWhere';
import { TaskCompletionBox } from '../views/nodes/TaskCompletedBox';
import { DocumentType } from './DocumentTypes';
import { Docs, DocumentOptions } from './Documents';
-
-// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
-const { DFLT_IMAGE_NATIVE_DIM } = require('../views/global/globalCssVariables.module.scss'); // prettier-ignore
-
-const defaultNativeImageDim = 10000000; //Number(DFLT_IMAGE_NATIVE_DIM.replace('px', ''));
+import { DocumentView } from '../views/nodes/DocumentView';
+import { CollectionFreeFormView } from '../views/collections/collectionFreeForm';
export namespace DocUtils {
function matchFieldValue(doc: Doc, key: string, valueIn: unknown): boolean {
@@ -381,6 +378,11 @@ export namespace DocUtils {
}, StrCast(dragDoc.title)),
icon: Doc.toIcon(dragDoc),
})) as ContextMenuProps[];
+ documentList.push({
+ description: ':Smart Drawing',
+ event: e => (DocumentView.Selected().lastElement().ComponentView as CollectionFreeFormView)?.showSmartDraw(e?.x || 0, e?.y || 0),
+ icon: 'file',
+ });
ContextMenu.Instance.addItem({
description: 'Create document',
subitems: documentList,
@@ -624,7 +626,7 @@ export namespace DocUtils {
export function assignImageInfo(result: Upload.FileInformation, protoIn: Doc) {
const proto = protoIn;
if (Upload.isImageInformation(result)) {
- const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim);
+ const maxNativeDim = Math.max(result.nativeHeight, result.nativeWidth);
const exifRotation = StrCast(result.exifData?.data?.Orientation).toLowerCase();
proto.data_nativeOrientation = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined);
proto.data_nativeWidth = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim;
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 56d505681..59a121de7 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -29,6 +29,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 3cbf3cc60..feaf41dfc 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -1,5 +1,3 @@
-/* eslint-disable prefer-destructuring */
-/* eslint-disable default-param-last */
/* eslint-disable no-use-before-define */
import { reaction } from 'mobx';
import { basename } from 'path';
@@ -672,7 +670,6 @@ export namespace Docs {
* only when creating a DockDocument from the current user's already existing
* main document.
*/
- // eslint-disable-next-line default-param-last
function InstanceFromProto(proto: Doc, data: FieldType | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDocIn?: Doc, noView?: boolean) {
const placeholderDoc = placeholderDocIn;
const viewKeys = ['x', 'y', 'isSystem']; // keys that should be addded to the view document even though they don't begin with an "_"
@@ -733,7 +730,6 @@ export namespace Docs {
return dataDoc;
}
- // eslint-disable-next-line default-param-last
export function ImageDocument(url: string | ImageField, options: DocumentOptions = {}, overwriteDoc?: Doc) {
const imgField = url instanceof ImageField ? url : url ? new ImageField(url) : undefined;
return InstanceFromProto(Prototypes.get(DocumentType.IMG), imgField, { title: basename(imgField?.url.href ?? '-no image-'), ...options }, undefined, undefined, undefined, overwriteDoc);
@@ -752,7 +748,6 @@ export namespace Docs {
* @param fieldKey the field that the compiled script is written into.
* @returns the Scripting Doc
*/
- // eslint-disable-next-line default-param-last
export function ScriptingDocument(script: Opt<ScriptField> | null, options: DocumentOptions = {}, fieldKey?: string) {
return InstanceFromProto(Prototypes.get(DocumentType.SCRIPTING), script || undefined, { ...options, layout: fieldKey ? `<ScriptingBox {...props} fieldKey={'${fieldKey}'}/>` /* ScriptingBox.LayoutString(fieldKey) */ : undefined });
}
@@ -760,7 +755,6 @@ export namespace Docs {
export function ChatDocument(options?: DocumentOptions) {
return InstanceFromProto(Prototypes.get(DocumentType.CHAT), undefined, { ...(options || {}) });
}
- // eslint-disable-next-line default-param-last
export function VideoDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
return InstanceFromProto(Prototypes.get(DocumentType.VID), new VideoField(url), options, undefined, undefined, undefined, overwriteDoc);
}
@@ -776,11 +770,10 @@ 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);
}
- // eslint-disable-next-line default-param-last
export function AudioDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
return InstanceFromProto(Prototypes.get(DocumentType.AUDIO), new AudioField(url), options, undefined, undefined, undefined, overwriteDoc);
}
@@ -835,7 +828,6 @@ export namespace Docs {
return InstanceFromProto(Prototypes.get(DocumentType.RTF), field, options, undefined, fieldKey);
}
- // eslint-disable-next-line default-param-last
export function LinkDocument(source: Doc, target: Doc, options: DocumentOptions = {}, id?: string) {
const linkDoc = InstanceFromProto(
Prototypes.get(DocumentType.LINK),
@@ -879,7 +871,6 @@ export namespace Docs {
return ink;
}
- // eslint-disable-next-line default-param-last
export function PdfDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) {
const width = options._width || undefined;
const height = options._height || undefined;
@@ -1034,6 +1025,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 03975a5e7..e7a4cbeeb 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -2,7 +2,7 @@
import { reaction, runInAction } from "mobx";
import * as rp from 'request-promise';
import { ClientUtils, OmitKeys } from "../../ClientUtils";
-import { Doc, DocListCast, DocListCastAsync, FieldType, Opt, StrListCast } from "../../fields/Doc";
+import { Doc, DocListCast, DocListCastAsync, FieldType, Opt } from "../../fields/Doc";
import { DocData } from "../../fields/DocSymbols";
import { InkTool } from "../../fields/InkField";
import { List } from "../../fields/List";
@@ -159,6 +159,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, _chromeHidden: true,
+ _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") {
@@ -366,7 +386,7 @@ pie title Minerals in my tap water
{key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, title_custom: true, waitForDoubleClickToClick: 'never'}, scripts: {onClick: FollowLinkScript()?.script.originalScript ?? ""}},
{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: "Chat", creator: Docs.Create.ChatDocument, opts: { _width: 300, _height: 300, }},
{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, _layout_dontCenter:'xy', dropAction: dropActionType.embed, treeView_HideTitle: true, _layout_fitWidth:true, layout_boxShadow: "0 0" }},
@@ -381,9 +401,7 @@ pie title Minerals in my tap water
];
emptyThings.forEach(
- thing =>{ DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, thing.scripts, thing.funcs);
- console.log(thing.key)
- });
+ thing => DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, thing.scripts, thing.funcs));
return [
{ toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, clickFactory: DocCast(doc.emptyNote)},
@@ -393,11 +411,11 @@ 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)},
- { toolTip: "Tap or drag to create a diagram", title: "Diagram", icon: "tree", dragFactory: doc.emptyDiagram as Doc, clickFactory: DocCast(doc.emptyDiagram)},
+ { toolTip: "Tap or drag to create a diagram", title: "Diagram", icon: "tree", dragFactory: doc.emptyDiagram as Doc, clickFactory: DocCast(doc.emptyDiagram)},
{ toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, clickFactory: DocCast(doc.emptyAudio), openFactoryLocation: OpenWhere.overlay},
{ toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, clickFactory: DocCast(doc.emptyMap)},
{ toolTip: "Tap or drag to create a chat assistant", title: "Assistant Chat", icon: "book",dragFactory: doc.emptyChat as Doc, clickFactory: DocCast(doc.emptyChat)},
@@ -406,9 +424,9 @@ pie title Minerals in my tap water
{ toolTip: "Tap or drag to create a button", title: "Button", icon: "circle", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)},
{ toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript), funcs: { hidden: "IsNoviceMode()"}},
{ toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "chart-bar", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)},
- { 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 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: "Toggle a Calculator REPL", title: "replviewer", icon: "calculator", clickFactory: '<ScriptingRepl />' as unknown as Doc, 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 => (
@@ -675,8 +693,9 @@ pie title Minerals in my tap water
{ title: "Type", icon:"eye", toolTip:"Sort by document type", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"docType", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
{ title: "Color", icon:"palette", toolTip:"Sort by document color", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"color", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
{ title: "Tags", icon:"bolt", toolTip:"Sort by document's tags", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"tag", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "Pile", icon:"layer-group", toolTip:"View the cards as a pile in the free form view!",btnType: ButtonType.ClickButton, expertMode: false, toolType:"pile", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
- { title: "Chat Popup",icon:"lightbulb", toolTip:"Toggle the chat popup's visibility!", width: 45, btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-chat",funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
+ { title: "Pile", icon:"layer-group", toolTip:"View the cards as a pile in the free form view", btnType: ButtonType.ClickButton, expertMode: false, toolType:"pile", funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'}},
+ { title: "Chat Popup",icon:"lightbulb", toolTip:"Toggle the chat popup's visibility", width: 45, btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-chat",funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
+ { title: "Show Tags", icon:"id-card", toolTip:"Toggle tag annotation panel", width: 45, btnType: ButtonType.ToggleButton, expertMode: false, toolType:"toggle-tags",funcs: {}, scripts: { onClick: '{ return showFreeform(this.toolType, _readOnly_);}'} },
{ title: "Sort", icon: "sort" , toolTip: "Manage sort order / lock status", btnType: ButtonType.MultiToggleButton, toolType:"alignment", ignoreClick: true,
subMenu: [
@@ -721,7 +740,7 @@ pie title Minerals in my tap water
{ title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Vcenter", toolTip: "Vertical center", btnType: ButtonType.ToggleButton, icon: "pallet", toolType:"vcent", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
- { title: "Align", toolTip: "Alignment", btnType: ButtonType.MultiToggleButton, toolType:"alignment", ignoreClick: true,
+ { title: "Align", toolTip: "Alignment", btnType: ButtonType.MultiToggleButton, toolType:"alignment",ignoreClick: true,
subMenu: [
{ title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
{ title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center",ignoreClick: true, scripts: {onClick: '{ return toggleCharStyle(this.toolType, _readOnly_);}'} },
@@ -738,12 +757,13 @@ pie title Minerals in my tap water
static inkTools():Button[] {
return [
- { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: "pen", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
- { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: "write", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
- { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {toolType:"activeEraserTool()"},
+ { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: "pen", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
+ { title: "Highlight",toolTip: "Highlight (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter",toolType: "highlighter", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }},
+ { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: "write", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }},
+ { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, toolType: InkTool.Eraser, scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' },
subMenu: [
{ title: "Stroke", toolTip: "Stroke Erase", btnType: ButtonType.ToggleButton, icon: "eraser", toolType:InkTool.StrokeEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
- { title: "Segment", toolTip: "Segment Erase", btnType: ButtonType.ToggleButton, icon: "xmark",toolType:InkTool.SegmentEraser,ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
+ { title: "Segment", toolTip: "Segment Erase", btnType: ButtonType.ToggleButton, icon: "xmark", toolType:InkTool.SegmentEraser,ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
{ title: "Radius", toolTip: "Radius Erase", btnType: ButtonType.ToggleButton, icon: "circle-xmark",toolType:InkTool.RadiusEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} },
]},
{ title: "Eraser Width", toolTip: "Eraser Width", btnType: ButtonType.NumberSliderButton, toolType: "eraserWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1, funcs: {hidden:"NotRadiusEraser()"}},
@@ -751,9 +771,10 @@ pie title Minerals in my tap water
{ title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType: Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
{ title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType: Gestures.Line, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} },
{ title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, funcs: {hidden:"IsNoviceMode()" } },
- { title: "Labels", toolTip: "Lab els", btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
+ { title: "Labels", toolTip: "Labels", btnType: ButtonType.ToggleButton, icon: "text-width", toolType: "labels", scripts: {onClick:'{ return setInkProperty(this.toolType, value, _readOnly_);}'}, },
{ title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1},
{ title: "Ink", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'} },
+ { title: "Smart Draw", toolTip: "Draw with GPT", btnType: ButtonType.ToggleButton, icon: "user-pen", toolType: "smartdraw", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {hidden: "IsNoviceMode()"}},
];
}
@@ -782,7 +803,7 @@ pie title Minerals in my tap water
{ title: "Rotate",toolTip: "Rotate 90", btnType: ButtonType.ClickButton, icon: "redo-alt", scripts: { onClick: 'imageRotate90();' }},
];
}
- static contextMenuTools():Button[] {
+ static contextMenuTools(doc:Doc):Button[] {
return [
{ btnList: new List<string>([CollectionViewType.Freeform, CollectionViewType.Schema, CollectionViewType.Tree,
CollectionViewType.Stacking, CollectionViewType.Masonry, CollectionViewType.Multicolumn,
@@ -799,6 +820,7 @@ pie title Minerals in my tap water
{ title: "Num", icon:"", toolTip: "Frame # (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}},
{ title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, width: 30, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}},
+ { title: "Filter", icon: "=", toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.tagGroupTools(),ignoreClick:true, toolType:DocumentType.COL, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 30, backgroundColor: doc.userVariantColor as string},
{ title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
{ title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: {hidden: `IsExploreMode()`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available
{ title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode, true)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Always available
@@ -811,8 +833,7 @@ pie title Minerals in my tap water
{ title: "Video", icon: "Video", toolTip: "Video functions", subMenu: CurrentUserUtils.videoTools(), expertMode: false, toolType:DocumentType.VID, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when video is selected
{ title: "Image", icon: "Image", toolTip: "Image functions", subMenu: CurrentUserUtils.imageTools(), expertMode: false, toolType:DocumentType.IMG, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`} }, // Only when image is selected
{ title: "Schema", icon: "Schema", toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectedDocType(this.toolType, this.expertMode)`, linearView_IsOpen: `SelectedDocType(this.toolType, this.expertMode)`}, linearBtnWidth:58 }, // Only when Schema is selected
- { title: "Filter", icon: "=", toolTip: "Filter cards by tags", subMenu: CurrentUserUtils.tagGroupTools(),ignoreClick:true, toolType:DocumentType.COL, funcs: {hidden: '!SelectedDocType(this.toolType, this.expertMode)'}, btnType: ButtonType.MultiToggleButton, width: 150},
- ];
+ ];
}
/// initializes a context menu button for the top bar context menu
@@ -857,7 +878,7 @@ pie title Minerals in my tap water
static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") {
const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", undoIgnoreFields:new List<string>(['width', "linearView_IsOpen"]), flexGap: 0, childDragAction: dropActionType.embed, childDontRegisterViews: true, linearView_IsOpen: true, ignoreClick: true, linearView_Expandable: false, _height: 35 };
const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined);
- const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) );
+ const ctxtMenuBtns = CurrentUserUtils.contextMenuTools(doc).map(params => this.setupContextMenuBtn(params, ctxtMenuBtnsDoc) );
return DocUtils.AssignOpts(ctxtMenuBtnsDoc, reqdCtxtOpts, ctxtMenuBtns);
}
/// Initializes all the default buttons for the top bar context menu
@@ -996,6 +1017,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 eb2011b77..b5d29be4c 100644
--- a/src/client/util/DropConverter.ts
+++ b/src/client/util/DropConverter.ts
@@ -5,7 +5,7 @@ import { RichTextField } from '../../fields/RichTextField';
import { ComputedField, ScriptField } from '../../fields/ScriptField';
import { StrCast } from '../../fields/Types';
import { ImageField } from '../../fields/URLField';
-import { Docs } from '../documents/Documents';
+import { Docs, DocumentOptions } from '../documents/Documents';
import { DocumentType } from '../documents/DocumentTypes';
import { ButtonType, FontIconBox } from '../views/nodes/FontIconBox/FontIconBox';
import { DragManager } from './DragManager';
@@ -64,29 +64,31 @@ export function MakeTemplate(doc: Doc) {
return doc;
}
-export function makeUserTemplateButton(doc: Doc) {
+/**
+ * Makes a draggable button or image that will create a template doc Instance
+ */
+export function makeUserTemplateButtonOrImage(doc: Doc, image?: string) {
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.FontIconDocument({
+ const docOptions: DocumentOptions = {
_nativeWidth: 100,
_nativeHeight: 100,
_width: 100,
_height: 100,
- backgroundColor: StrCast(doc.backgroundColor),
title: StrCast(layoutDoc.title),
- btnType: ButtonType.ClickButton,
- icon: 'bolt',
isSystem: false,
- });
+ };
+ const dbox = image ? Docs.Create.ImageDocument(image, docOptions) : Docs.Create.FontIconDocument({ ...docOptions, backgroundColor: StrCast(doc.backgroundColor), btnType: ButtonType.ClickButton, icon: 'bolt' });
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;
@@ -102,7 +104,7 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) {
});
}
} else if (!doc.onDragStart && !doc.isButtonBar) {
- dbox = makeUserTemplateButton(doc);
+ dbox = makeUserTemplateButtonOrImage(doc);
} else if (doc.isButtonBar) {
dbox.ignoreClick = true;
}
diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts
index 47d85a723..c7b86815a 100644
--- a/src/client/util/Scripting.ts
+++ b/src/client/util/Scripting.ts
@@ -1,7 +1,7 @@
// export const ts = (window as any).ts;
// import * as typescriptlib from '!!raw-loader!../../../node_modules/typescript/lib/lib.d.ts'
// import * as typescriptes5 from '!!raw-loader!../../../node_modules/typescript/lib/lib.es5.d.ts'
-// import typescriptlib from 'type_decls.d';
+import typescriptlib from 'type_decls.d';
import * as ts from 'typescript';
import { Doc, FieldType } from '../../fields/Doc';
import { RefField } from '../../fields/RefField';
@@ -60,7 +60,6 @@ function Run(script: string | undefined, customParams: string[], diagnostics: ts
// let params: any[] = [Docs, ...fieldTypes];
const compiledFunction = (() => {
try {
- // eslint-disable-next-line no-new-func
return new Function(...paramNames, `return ${script}`);
} catch (e) {
console.log(e);
@@ -69,10 +68,8 @@ function Run(script: string | undefined, customParams: string[], diagnostics: ts
})();
if (!compiledFunction) return { compiled: false, errors };
const { capturedVariables = {} } = options;
- // eslint-disable-next-line default-param-last
const run = (args: { [name: string]: unknown } = {}, onError?: (e: string) => void, errorVal?: ts.Diagnostic): ScriptResult => {
const argsArray: unknown[] = [];
- // eslint-disable-next-line no-restricted-syntax
for (const name of customParams) {
if (name !== 'this') {
argsArray.push(name in args ? args[name] : capturedVariables[name]);
@@ -224,7 +221,6 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
if ('this' in params || 'this' in capturedVariables) {
paramNames.push('this');
}
- // eslint-disable-next-line no-restricted-syntax
for (const key in params) {
if (key !== 'this') {
paramNames.push(key);
@@ -234,7 +230,6 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp
const val = params[key];
return `${key}: ${val}`;
});
- // eslint-disable-next-line no-restricted-syntax
for (const key in capturedVariables) {
if (key !== 'this') {
const val = capturedVariables[key];
diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx
index fde8869e3..9200d68db 100644
--- a/src/client/util/SettingsManager.tsx
+++ b/src/client/util/SettingsManager.tsx
@@ -85,7 +85,7 @@ export class SettingsManager extends React.Component<object> {
if (this._playgroundMode) {
DocServer.Control.makeReadOnly();
addStyleSheetRule(SettingsManager._settingsStyle, 'topbar-inner-container', { background: 'red !important' });
- } else ClientUtils.CurrentUserEmail() !== 'guest' && DocServer.Control.makeEditable();
+ } else if (ClientUtils.CurrentUserEmail() !== 'guest') DocServer.Control.makeEditable();
}),
'set playgorund mode'
);
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
index d6f3f2340..4aef28e6b 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -4,6 +4,15 @@
/* eslint-disable camelcase */
import { Point } from '../../pen-gestures/ndollar';
+export enum SVGType {
+ Rect = 'rect',
+ Path = 'path',
+ Circle = 'circle',
+ Ellipse = 'ellipse',
+ Line = 'line',
+ Polygon = 'polygon',
+}
+
class SmartRect {
minx: number = 0;
miny: number = 0;
@@ -557,6 +566,12 @@ function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2:
const negThatCenter = new Point(-tHatCenter.X, -tHatCenter.Y);
FitCubic(d, splitPoint2D, last, negThatCenter, tHat2, error, result);
}
+/**
+ * Convert polyline coordinates to a (multi) segment bezier curve
+ * @param d - polyline coordinates
+ * @param error - how much error to allow in fitting (measured in pixels)
+ * @returns
+ */
export function FitCurve(d: Point[], error: number) {
const tHat1 = ComputeLeftTangent(d, 0); // Unit tangent vectors at endpoints
const tHat2 = ComputeRightTangent(d, d.length - 1);
@@ -586,6 +601,151 @@ 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 = startPt;
+ 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);
+ 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/ContextMenu.tsx b/src/client/views/ContextMenu.tsx
index 0e98d2e35..2eb3e5565 100644
--- a/src/client/views/ContextMenu.tsx
+++ b/src/client/views/ContextMenu.tsx
@@ -1,6 +1,3 @@
-/* eslint-disable react/no-array-index-key */
-/* eslint-disable react/jsx-props-no-spreading */
-/* eslint-disable default-param-last */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -257,7 +254,7 @@ export class ContextMenu extends ObservableReactComponent<{ noexpand?: boolean }
this._selectedIndex--;
}
e.preventDefault();
- } else if (e.key === 'Enter' || e.key === 'Tab') {
+ } else if ((e.key === 'Enter' || e.key === 'Tab') && this._selectedIndex >= 0) {
const item = this.flatItems[this._selectedIndex];
if (item.event) {
item.event({ x: this.pageX, y: this.pageY });
diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx
index 5b4eb704b..5d31173e1 100644
--- a/src/client/views/ContextMenuItem.tsx
+++ b/src/client/views/ContextMenuItem.tsx
@@ -18,7 +18,7 @@ export interface ContextMenuProps {
noexpand?: boolean; // whether to render the submenu items as a flyout from this item, or inline in place of this item
undoable?: boolean; // whether to wrap the event callback in an UndoBatch or not
- event?: (stuff?: unknown) => void;
+ event?: (stuff?: { x: number; y: number }) => void;
}
@observer
diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx
index eced64524..448178397 100644
--- a/src/client/views/DashboardView.tsx
+++ b/src/client/views/DashboardView.tsx
@@ -433,15 +433,15 @@ export class DashboardView extends ObservableReactComponent<object> {
dashboardDoc[DocData].myPublishedDocs = new List<Doc>();
dashboardDoc[DocData].myTagCollections = new List<Doc>();
dashboardDoc[DocData].myUniqueFaces = new List<Doc>();
- dashboardDoc[DocData].myTrails = DashboardView.SetupDashboardTrails(dashboardDoc);
- dashboardDoc[DocData].myCalendars = DashboardView.SetupDashboardCalendars(dashboardDoc);
+ dashboardDoc[DocData].myTrails = DashboardView.SetupDashboardTrails();
+ dashboardDoc[DocData].myCalendars = DashboardView.SetupDashboardCalendars();
// open this new dashboard
Doc.ActiveDashboard = dashboardDoc;
Doc.ActivePage = 'dashboard';
Doc.ActivePresentation = undefined;
};
- public static SetupDashboardCalendars(dashboardDoc: Doc) {
+ public static SetupDashboardCalendars() {
// this section is creating the button document itself === myTrails = new Button
// create a a list of calendars (as a CalendarCollectionDocument) and store it on the new dashboard
@@ -470,7 +470,7 @@ export class DashboardView extends ObservableReactComponent<object> {
return new PrefetchProxy(myCalendars);
}
- public static SetupDashboardTrails(dashboardDoc: Doc) {
+ public static SetupDashboardTrails() {
// this section is creating the button document itself === myTrails = new Button
const reqdBtnOpts: DocumentOptions = {
_forceActive: true,
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 34b05da56..1c0d51e17 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -2,7 +2,7 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
import { IconButton } from 'browndash-components';
-import { action, computed, makeObservable, observable, runInAction, trace } from 'mobx';
+import { action, computed, makeObservable, observable, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { FaUndo } from 'react-icons/fa';
@@ -36,7 +36,6 @@ import { ImageBox } from './nodes/ImageBox';
import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { TagsView } from './TagsView';
-import { setTime } from 'react-datepicker/dist/date_utils';
interface DocumentDecorationsProps {
PanelWidth: number;
@@ -60,7 +59,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
private _interactionLock?: boolean;
@observable _showNothing = true;
- @observable private _forceRender = 0
+ @observable private _forceRender = 0;
@observable private _accumulatedTitle = '';
@observable private _titleControlString: string = '$title';
@observable private _editingTitle = false;
@@ -232,10 +231,17 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
views.forEach(iconView => {
const iconViewDoc = iconView.Document;
Doc.setNativeView(iconViewDoc);
+ // bcz: hacky ... when closing a Doc do different things depending on the contet ...
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.
+ iconViewDoc.opacity = 0; // if in an animation collection, set opacity to 0 to allow inkMasks and other documents to remain in the collection and to smoothly animate when they are activated in a different animation frame
} else {
+ // if Doc is in the annotation palette, remove the flag indicating that it's saved
+ const dragFactory = DocCast(iconView.Document.dragFactory);
+ if (dragFactory && DocCast(dragFactory.cloneOf).savedAsAnno) DocCast(dragFactory.cloneOf).savedAsAnno = undefined;
+
+ // if this is a face Annotation doc, then just hide it.
if (iconView.Document.annotationOn && iconView.Document.face) iconView.Document.hidden = true;
+ // otherwise actually remove the Doc from its parent collection
else iconView._props.removeDocument?.(iconView.Document);
}
});
@@ -643,7 +649,6 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
}
return this._rotCenter;
}
-;
render() {
this._forceRender;
const { b, r, x, y } = this.Bounds;
@@ -659,7 +664,7 @@ export class DocumentDecorations extends ObservableReactComponent<DocumentDecora
}
if (seldocview && !seldocview?.ContentDiv?.getBoundingClientRect().width) {
- setTimeout(action(() => this._forceRender++)); // if the selected Doc has no width, then assume it's stil being layed out and try to render again later.
+ setTimeout(action(() => this._forceRender++)); // if the selected Doc has no width, then assume it's stil being layed out and try to render again later.
return null;
}
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index 3a2738c3b..5fddaec9a 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -3,34 +3,40 @@ import { action, computed, makeObservable, observable, runInAction } from 'mobx'
import { observer } from 'mobx-react';
import * as React from 'react';
import { returnEmptyFilter, returnEmptyString, returnFalse, setupMoveUpEvents } from '../../ClientUtils';
-import { emptyFunction } from '../../Utils';
+import { emptyFunction, intersectRect } from '../../Utils';
import { Doc, Opt, returnEmptyDoclist } from '../../fields/Doc';
import { InkData, InkField, InkTool } from '../../fields/InkField';
import { NumCast } from '../../fields/Types';
+import { Gestures } from '../../pen-gestures/GestureTypes';
+import { GestureUtils } from '../../pen-gestures/GestureUtils';
+import { Result } from '../../pen-gestures/ndollar';
+import { DocumentType } from '../documents/DocumentTypes';
+import { Docs } from '../documents/Documents';
+import { InteractionUtils } from '../util/InteractionUtils';
+import { ScriptingGlobals } from '../util/ScriptingGlobals';
+import { Transform } from '../util/Transform';
+import { undoable } from '../util/UndoManager';
+import './GestureOverlay.scss';
+import { InkingStroke } from './InkingStroke';
+import { ObservableReactComponent } from './ObservableReactComponent';
+import { returnEmptyDocViewList } from './StyleProvider';
+import { CollectionFreeFormView } from './collections/collectionFreeForm';
import {
ActiveArrowEnd,
ActiveArrowScale,
ActiveArrowStart,
ActiveDash,
+ ActiveFillColor,
ActiveInkBezierApprox,
ActiveInkColor,
ActiveInkWidth,
+ DocumentView,
SetActiveArrowStart,
SetActiveDash,
SetActiveFillColor,
SetActiveInkColor,
SetActiveInkWidth,
} from './nodes/DocumentView';
-import { Gestures } from '../../pen-gestures/GestureTypes';
-import { GestureUtils } from '../../pen-gestures/GestureUtils';
-import { InteractionUtils } from '../util/InteractionUtils';
-import { ScriptingGlobals } from '../util/ScriptingGlobals';
-import { Transform } from '../util/Transform';
-import './GestureOverlay.scss';
-import { ObservableReactComponent } from './ObservableReactComponent';
-import { returnEmptyDocViewList } from './StyleProvider';
-import { ActiveFillColor, DocumentView } from './nodes/DocumentView';
-
export enum ToolglassTools {
InkToText = 'inktotext',
IgnoreGesture = 'ignoregesture',
@@ -41,6 +47,10 @@ interface GestureOverlayProps {
isActive: boolean;
}
@observer
+/**
+ * class for gestures. will determine if what the user drew is a gesture, and will transform the ink stroke into the shape the user
+ * drew or perform the gesture's action
+ */
export class GestureOverlay extends ObservableReactComponent<React.PropsWithChildren<GestureOverlayProps>> {
// eslint-disable-next-line no-use-before-define
static Instance: GestureOverlay;
@@ -70,10 +80,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
}
private _overlayRef = React.createRef<HTMLDivElement>();
- private _d1: Doc | undefined;
- private _inkToTextDoc: Doc | undefined;
- private thumbIdentifier?: number;
- private pointerIdentifier?: number;
constructor(props: GestureOverlayProps) {
super(props);
@@ -88,7 +94,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
componentDidMount() {
GestureOverlay.Instance = this;
}
-
@action
onPointerDown = (e: React.PointerEvent) => {
if (!(e.target as HTMLElement)?.className?.toString().startsWith('lm_')) {
@@ -127,81 +132,249 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
// SetActiveArrowEnd('none');
}
}
+ /**
+ * If what the user drew is a scribble, this returns the documents that were scribbled over
+ * I changed it so it doesnt use triangles. It will modify an intersect array, with its length being
+ * how many sharp cusps there are. The first index will have a boolean that is based on if there is an
+ * intersection in the first 1/length percent of the stroke. The second index will be if there is an intersection
+ * in the 2nd 1/length percent of the stroke. This array will be used in determineIfScribble().
+ * @param ffview freeform view where scribble is drawn
+ * @param scribbleStroke scribble stroke in screen space coordinats
+ * @returns array of documents scribbled over
+ */
+ isScribble = (ffView: CollectionFreeFormView, cuspArray: { X: number; Y: number }[], scribbleStroke: { X: number; Y: number }[]) => {
+ const intersectArray = cuspArray.map(() => false);
+ const scribbleBounds = InkField.getBounds(scribbleStroke);
+ const docsToDelete = ffView.childDocs
+ .map(doc => DocumentView.getDocumentView(doc))
+ .filter(dv => dv?.ComponentView instanceof InkingStroke)
+ .map(dv => dv?.ComponentView as InkingStroke)
+ .filter(otherInk => {
+ const otherScreenPts = otherInk.inkScaledData?.().inkData.map(otherInk.ptToScreen);
+ if (intersectRect(InkField.getBounds(otherScreenPts), scribbleBounds)) {
+ const intersects = this.findInkIntersections(scribbleStroke, otherScreenPts).map(intersect => {
+ const percentage = intersect.split('/')[0];
+ intersectArray[Math.floor(Number(percentage) * cuspArray.length)] = true;
+ });
+ return intersects.length > 0;
+ }
+ });
+ return !this.determineIfScribble(intersectArray) ? undefined :
+ [ ...docsToDelete.map(stroke => stroke.Document),
+ // bcz: NOTE: docsInBoundingBox test should be replaced with a docsInConvexHull test
+ ...this.docsInBoundingBox({ topLeft : ffView.ScreenToContentsXf().transformPoint(scribbleBounds.left, scribbleBounds.top),
+ bottomRight: ffView.ScreenToContentsXf().transformPoint(scribbleBounds.right,scribbleBounds.bottom)},
+ ffView.childDocs.filter(doc => !docsToDelete.map(s => s.Document).includes(doc)) )]; // prettier-ignore
+ };
+ /**
+ * Returns all docs in array that overlap bounds. Note that the bounds should be given in screen space coordinates.
+ * @param boundingBox screen space bounding box
+ * @param childDocs array of docs to test against bounding box
+ * @returns list of docs that overlap rect
+ */
+ docsInBoundingBox = (boundingBox: { topLeft: number[]; bottomRight: number[] }, childDocs: Doc[]): Doc[] => {
+ const rect = { left: boundingBox.topLeft[0], top: boundingBox.topLeft[1], width: boundingBox.bottomRight[0] - boundingBox.topLeft[0], height: boundingBox.bottomRight[1] - boundingBox.topLeft[1] };
+ return childDocs.filter(doc => intersectRect(rect, { left: NumCast(doc.x), top: NumCast(doc.y), width: NumCast(doc._width), height: NumCast(doc._height) }));
+ };
+ /**
+ * Determines if what the array of cusp/intersection data corresponds to a scribble.
+ * true if there are at least 4 cusps and either:
+ * 1) the initial and final quarters of the array contain objects
+ * 2) or half of the cusps contain objects
+ * @param intersectArray array of booleans coresponding to which scribble sections (regions separated by a cusp) contain Docs
+ * @returns
+ */
+ determineIfScribble = (intersectArray: boolean[]) => {
+ const quarterArrayLength = Math.ceil(intersectArray.length / 3.9); // use 3.9 instead of 4 to work better with strokes with only 4 cusps
+ const { start, end } = intersectArray.reduce((res, val, i) => ({ // test for scribbles at start and end of scribble stroke
+ start: res.start || (val && i <= quarterArrayLength),
+ end: res.end || (val && i >= intersectArray.length - quarterArrayLength)
+ }), { start: false, end: false }); // prettier-ignore
+
+ const percentCuspsWithContent = intersectArray.filter(value => value).length / intersectArray.length;
+ return intersectArray.length > 3 && (percentCuspsWithContent >= 0.5 || (start && end));
+ };
+ /**
+ * determines if inks intersect
+ * @param line is pointData
+ * @param triangle triangle with 3 points
+ * @returns will return an array, with its lenght being equal to how many intersections there are betweent the 2 strokes.
+ * each item in the array will contain a number between 0-1 or a number 0-1 seperated by a comma. If one of the curves is a line, then
+ * then there will just be a number that reprents how far that intersection is along the scribble. For example,
+ * .1 means that the intersection occurs 10% into the scribble, so near the beginning of it. but if they are both curves, then
+ * it will return two numbers, one for each curve, seperated by a comma. Sometimes, the percentage it returns is inaccurate,
+ * espcially in the beginning and end parts of the stroke. dont know why. hope this makes sense
+ */
+ findInkIntersections = (scribble: InkData, inkStroke: InkData): string[] => {
+ const intersectArray: string[] = [];
+ const scribbleBounds = InkField.getBounds(scribble);
+ for (let i = 0; i < scribble.length - 3; i += 4) { // for each segment of scribble
+ for (let j = 0; j < inkStroke.length - 3; j += 4) { // for each segment of ink stroke
+ const scribbleSeg = InkField.Segment(scribble, i);
+ const strokeSeg = InkField.Segment(inkStroke, j);
+ const strokeBounds = InkField.getBounds(strokeSeg.points.map(pt => ({ X: pt.x, Y: pt.y })));
+ if (intersectRect(scribbleBounds, strokeBounds)) {
+ const result = InkField.bintersects(scribbleSeg, strokeSeg)[0];
+ if (result !== undefined) {
+ intersectArray.push(result.toString());
+ }
+ }
+ } // prettier-ignore
+ } // prettier-ignore
+ return intersectArray;
+ };
+ dryInk = () => {
+ const newPoints = this._points.reduce((p, pts) => {
+ p.push([pts.X, pts.Y]);
+ return p;
+ }, [] as number[][]);
+ newPoints.pop();
+ const controlPoints: { X: number; Y: number }[] = [];
+
+ const bezierCurves = fitCurve.default(newPoints, 10);
+ Array.from(bezierCurves).forEach(curve => {
+ controlPoints.push({ X: curve[0][0], Y: curve[0][1] });
+ controlPoints.push({ X: curve[1][0], Y: curve[1][1] });
+ controlPoints.push({ X: curve[2][0], Y: curve[2][1] });
+ controlPoints.push({ X: curve[3][0], Y: curve[3][1] });
+ });
+ const dist = Math.sqrt((controlPoints[0].X - controlPoints.lastElement().X) * (controlPoints[0].X - controlPoints.lastElement().X) + (controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y));
+ if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0];
+ this._points.length = 0;
+ this._points.push(...controlPoints);
+ this.dispatchGesture(Gestures.Stroke);
+ };
@action
onPointerUp = () => {
+ const ffView = DocumentView.DownDocView?.ComponentView instanceof CollectionFreeFormView && DocumentView.DownDocView.ComponentView;
DocumentView.DownDocView = undefined;
if (this._points.length > 1) {
const B = this.svgBounds;
const points = this._points.map(p => ({ X: p.X - B.left, Y: p.Y - B.top }));
-
+ const { Name, Score } =
+ (this.InkShape
+ ? new Result(this.InkShape, 1, Date.now)
+ : Doc.UserDoc().recognizeGestures && points.length > 2
+ ? GestureUtils.GestureRecognizer.Recognize([points])
+ : undefined) ??
+ new Result(Gestures.Stroke, 1, Date.now); // prettier-ignore
+
+ const cuspArray = this.getCusps(points);
// if any of the shape is activated in the CollectionFreeFormViewChrome
- if (this.InkShape) {
- this.makeBezierPolygon(this.InkShape, false);
- this.dispatchGesture(this.InkShape);
- this.primCreated();
- }
- // if we're not drawing in a toolglass try to recognize as gesture
- else {
- // need to decide when to turn gestures back on
- const result = points.length > 2 && GestureUtils.GestureRecognizer.Recognize([points]);
- let actionPerformed = false;
- if (Doc.UserDoc().recognizeGestures && result && result.Score > 0.7) {
- switch (result.Name) {
- case Gestures.Line:
- case Gestures.Triangle:
- case Gestures.Rectangle:
- case Gestures.Circle:
- this.makeBezierPolygon(result.Name, true);
- actionPerformed = this.dispatchGesture(result.Name);
- break;
- case Gestures.Scribble:
- console.log('scribble');
- break;
- default:
- }
+ // need to decide when to turn gestures back on
+ const actionPerformed = ((name: Gestures) => {
+ switch (name) {
+ case Gestures.Line:
+ if (cuspArray.length > 2) return undefined;
+ // eslint-disable-next-line no-fallthrough
+ case Gestures.Triangle:
+ case Gestures.Rectangle:
+ case Gestures.Circle:
+ this.makeBezierPolygon(this._points, Name, true);
+ return this.dispatchGesture(name);
+ case Gestures.RightAngle:
+ return ffView && this.convertToText(ffView).length > 0;
+ default:
}
-
- // if no gesture (or if the gesture was unsuccessful), "dry" the stroke into an ink document
- if (!actionPerformed) {
- const newPoints = this._points.reduce((p, pts) => {
- p.push([pts.X, pts.Y]);
- return p;
- }, [] as number[][]);
- newPoints.pop();
- const controlPoints: { X: number; Y: number }[] = [];
-
- const bezierCurves = fitCurve.default(newPoints, 10);
- Array.from(bezierCurves).forEach(curve => {
- controlPoints.push({ X: curve[0][0], Y: curve[0][1] });
- controlPoints.push({ X: curve[1][0], Y: curve[1][1] });
- controlPoints.push({ X: curve[2][0], Y: curve[2][1] });
- controlPoints.push({ X: curve[3][0], Y: curve[3][1] });
- });
- const dist = Math.sqrt(
- (controlPoints[0].X - controlPoints.lastElement().X) * (controlPoints[0].X - controlPoints.lastElement().X) + (controlPoints[0].Y - controlPoints.lastElement().Y) * (controlPoints[0].Y - controlPoints.lastElement().Y)
- );
- // eslint-disable-next-line prefer-destructuring
- if (controlPoints.length > 4 && dist < 10) controlPoints[controlPoints.length - 1] = controlPoints[0];
- this._points.length = 0;
- this._points.push(...controlPoints);
- this.dispatchGesture(Gestures.Stroke);
+ })(Score < 0.7 ? Gestures.Stroke : (Name as Gestures));
+ // if no gesture (or if the gesture was unsuccessful), "dry" the stroke into an ink document
+
+ if (!actionPerformed) {
+ const scribbledOver = ffView && this.isScribble(ffView, cuspArray, this._points);
+ if (scribbledOver) {
+ undoable(() => ffView.removeDocument(scribbledOver), 'scribble erase')();
+ } else {
+ this.dryInk();
}
}
}
this._points.length = 0;
};
-
- makeBezierPolygon = (shape: string, gesture: boolean) => {
+ /**
+ * used in the rightAngle gesture to convert handwriting into text. will only work on collections
+ * TODO: make it work on individual ink docs.
+ */
+ convertToText = (ffView: CollectionFreeFormView) => {
+ let minX = 999999999;
+ let maxX = -999999999;
+ let minY = 999999999;
+ let maxY = -999999999;
+ const textDocs: Doc[] = [];
+ ffView.childDocs
+ .filter(doc => doc.type === DocumentType.COL)
+ .forEach(doc => {
+ if (typeof doc.width === 'number' && typeof doc.height === 'number' && typeof doc.x === 'number' && typeof doc.y === 'number') {
+ const bounds = DocumentView.getDocumentView(doc)?.getBounds;
+ if (bounds) {
+ if (intersectRect({ ...bounds, width: bounds.right - bounds.left, height: bounds.bottom - bounds.top }, InkField.getBounds(this._points))) {
+ if (doc.x < minX) {
+ minX = doc.x;
+ }
+ if (doc.x > maxX) {
+ maxX = doc.x;
+ }
+ if (doc.y < minY) {
+ minY = doc.y;
+ }
+ if (doc.y + doc.height > maxY) {
+ maxY = doc.y + doc.height;
+ }
+ const newDoc = Docs.Create.TextDocument(doc.transcription as string, { title: '', x: doc.x as number, y: minY });
+ newDoc.height = doc.height;
+ newDoc.width = doc.width;
+ if (ffView.addDocument && ffView.removeDocument) {
+ ffView.addDocument(newDoc);
+ ffView.removeDocument(doc);
+ }
+ textDocs.push(newDoc);
+ }
+ }
+ }
+ });
+ return textDocs;
+ };
+ /**
+ * Returns array of coordinates corresponding to the sharp cusps in an input stroke
+ * @param points array of X,Y stroke coordinates
+ * @returns array containing the coordinates of the sharp cusps
+ */
+ getCusps(points: InkData) {
+ const arrayOfPoints: { X: number; Y: number }[] = [];
+ arrayOfPoints.push(points[0]);
+ for (let i = 0; i < points.length - 2; i++) {
+ const point1 = points[i];
+ const point2 = points[i + 1];
+ const point3 = points[i + 2];
+ if (this.find_angle(point1, point2, point3) < 90) {
+ // NOTE: this is not an accurate way to find cusps -- it is highly dependent on sampling rate and doesn't work well with slowly drawn scribbles
+ arrayOfPoints.push(point2);
+ }
+ }
+ arrayOfPoints.push(points[points.length - 1]);
+ return arrayOfPoints;
+ }
+ /**
+ * takes in three points and then determines the angle of the points. used to determine if the cusp
+ * is sharp enoug
+ * @returns
+ */
+ find_angle(A: { X: number; Y: number }, B: { X: number; Y: number }, C: { X: number; Y: number }) {
+ const AB = Math.sqrt(Math.pow(B.X - A.X, 2) + Math.pow(B.Y - A.Y, 2));
+ const BC = Math.sqrt(Math.pow(B.X - C.X, 2) + Math.pow(B.Y - C.Y, 2));
+ const AC = Math.sqrt(Math.pow(C.X - A.X, 2) + Math.pow(C.Y - A.Y, 2));
+ return Math.acos((BC * BC + AB * AB - AC * AC) / (2 * BC * AB)) * (180 / Math.PI);
+ }
+ makeBezierPolygon = (points: { X: number; Y: number }[], shape: string, gesture: boolean) => {
const xs = this._points.map(p => p.X);
const ys = this._points.map(p => p.Y);
let right = Math.max(...xs);
let left = Math.min(...xs);
let bottom = Math.max(...ys);
let top = Math.min(...ys);
- const firstx = this._points[0].X;
- const firsty = this._points[0].Y;
- let lastx = this._points[this._points.length - 2].X;
- let lasty = this._points[this._points.length - 2].Y;
+ const firstx = points[0].X;
+ const firsty = points[0].Y;
+ let lastx = points[points.length - 2].X;
+ let lasty = points[points.length - 2].Y;
let fourth = (lastx - firstx) / 4;
if (isNaN(fourth) || fourth === 0) {
fourth = 0.01;
@@ -212,15 +385,15 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
}
// const b = firsty - m * firstx;
if (shape === 'noRec') {
- return false;
+ return undefined;
}
if (!gesture) {
// if shape options is activated in inkOptionMenu
// take second to last point because _point[length-1] is _points[0]
- right = this._points[this._points.length - 2].X;
- left = this._points[0].X;
- bottom = this._points[this._points.length - 2].Y;
- top = this._points[0].Y;
+ right = points[points.length - 2].X;
+ left = points[0].X;
+ bottom = points[points.length - 2].Y;
+ top = points[0].Y;
if (shape !== Gestures.Arrow && shape !== Gestures.Line && shape !== Gestures.Circle) {
if (left > right) {
const temp = right;
@@ -234,47 +407,47 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
}
}
}
- this._points.length = 0;
+ points.length = 0;
switch (shape) {
case Gestures.Rectangle:
- this._points.push({ X: left, Y: top });
- this._points.push({ X: left, Y: top });
- this._points.push({ X: right, Y: top });
- this._points.push({ X: right, Y: top });
-
- this._points.push({ X: right, Y: top });
- this._points.push({ X: right, Y: top });
- this._points.push({ X: right, Y: bottom });
- this._points.push({ X: right, Y: bottom });
-
- this._points.push({ X: right, Y: bottom });
- this._points.push({ X: right, Y: bottom });
- this._points.push({ X: left, Y: bottom });
- this._points.push({ X: left, Y: bottom });
-
- this._points.push({ X: left, Y: bottom });
- this._points.push({ X: left, Y: bottom });
- this._points.push({ X: left, Y: top });
- this._points.push({ X: left, Y: top });
+ points.push({ X: left, Y: top }); // curr pt
+ points.push({ X: left, Y: top }); // curr first ctrl pt
+ points.push({ X: right, Y: top }); // next ctrl pt
+ points.push({ X: right, Y: top }); // next pt
+
+ points.push({ X: right, Y: top }); // next pt
+ points.push({ X: right, Y: top }); // next first ctrl pt
+ points.push({ X: right, Y: bottom }); // next next ctrl pt
+ points.push({ X: right, Y: bottom }); // next next pt
+
+ points.push({ X: right, Y: bottom });
+ points.push({ X: right, Y: bottom });
+ points.push({ X: left, Y: bottom });
+ points.push({ X: left, Y: bottom });
+
+ points.push({ X: left, Y: bottom });
+ points.push({ X: left, Y: bottom });
+ points.push({ X: left, Y: top });
+ points.push({ X: left, Y: top });
break;
case Gestures.Triangle:
- this._points.push({ X: left, Y: bottom });
- this._points.push({ X: left, Y: bottom });
+ points.push({ X: left, Y: bottom });
+ points.push({ X: left, Y: bottom });
- this._points.push({ X: right, Y: bottom });
- this._points.push({ X: right, Y: bottom });
- this._points.push({ X: right, Y: bottom });
- this._points.push({ X: right, Y: bottom });
+ points.push({ X: right, Y: bottom });
+ points.push({ X: right, Y: bottom });
+ points.push({ X: right, Y: bottom });
+ points.push({ X: right, Y: bottom });
- this._points.push({ X: (right + left) / 2, Y: top });
- this._points.push({ X: (right + left) / 2, Y: top });
- this._points.push({ X: (right + left) / 2, Y: top });
- this._points.push({ X: (right + left) / 2, Y: top });
+ points.push({ X: (right + left) / 2, Y: top });
+ points.push({ X: (right + left) / 2, Y: top });
+ points.push({ X: (right + left) / 2, Y: top });
+ points.push({ X: (right + left) / 2, Y: top });
- this._points.push({ X: left, Y: bottom });
- this._points.push({ X: left, Y: bottom });
+ points.push({ X: left, Y: bottom });
+ points.push({ X: left, Y: bottom });
break;
case Gestures.Circle:
@@ -288,25 +461,25 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
const radius = Math.max(centerX - Math.min(left, right), centerY - Math.min(top, bottom));
// Dividing the circle into four equal sections, and fitting each section to a cubic Bézier curve.
- this._points.push({ X: centerX, Y: centerY + radius });
- this._points.push({ X: centerX + c * radius, Y: centerY + radius });
- this._points.push({ X: centerX + radius, Y: centerY + c * radius });
- this._points.push({ X: centerX + radius, Y: centerY });
-
- this._points.push({ X: centerX + radius, Y: centerY });
- this._points.push({ X: centerX + radius, Y: centerY - c * radius });
- this._points.push({ X: centerX + c * radius, Y: centerY - radius });
- this._points.push({ X: centerX, Y: centerY - radius });
-
- this._points.push({ X: centerX, Y: centerY - radius });
- this._points.push({ X: centerX - c * radius, Y: centerY - radius });
- this._points.push({ X: centerX - radius, Y: centerY - c * radius });
- this._points.push({ X: centerX - radius, Y: centerY });
-
- this._points.push({ X: centerX - radius, Y: centerY });
- this._points.push({ X: centerX - radius, Y: centerY + c * radius });
- this._points.push({ X: centerX - c * radius, Y: centerY + radius });
- this._points.push({ X: centerX, Y: centerY + radius });
+ points.push({ X: centerX, Y: centerY + radius }); // curr pt
+ points.push({ X: centerX + c * radius, Y: centerY + radius }); // curr first ctrl pt
+ points.push({ X: centerX + radius, Y: centerY + c * radius }); // next pt ctrl pt
+ points.push({ X: centerX + radius, Y: centerY }); // next pt
+
+ points.push({ X: centerX + radius, Y: centerY }); // next pt
+ points.push({ X: centerX + radius, Y: centerY - c * radius }); // next first ctrl pt
+ points.push({ X: centerX + c * radius, Y: centerY - radius });
+ points.push({ X: centerX, Y: centerY - radius });
+
+ points.push({ X: centerX, Y: centerY - radius });
+ points.push({ X: centerX - c * radius, Y: centerY - radius });
+ points.push({ X: centerX - radius, Y: centerY - c * radius });
+ points.push({ X: centerX - radius, Y: centerY });
+
+ points.push({ X: centerX - radius, Y: centerY });
+ points.push({ X: centerX - radius, Y: centerY + c * radius });
+ points.push({ X: centerX - c * radius, Y: centerY + radius });
+ points.push({ X: centerX, Y: centerY + radius });
}
break;
@@ -317,11 +490,11 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
if (Math.abs(firsty - lasty) < 10 && Math.abs(firstx - lastx) > 10) {
lasty = firsty;
}
- this._points.push({ X: firstx, Y: firsty });
- this._points.push({ X: firstx, Y: firsty });
+ points.push({ X: firstx, Y: firsty });
+ points.push({ X: firstx, Y: firsty });
- this._points.push({ X: lastx, Y: lasty });
- this._points.push({ X: lastx, Y: lasty });
+ points.push({ X: lastx, Y: lasty });
+ points.push({ X: lastx, Y: lasty });
break;
case Gestures.Arrow:
{
@@ -336,16 +509,16 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
const y3 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) - (x1 - x2) * Math.sin(angle));
const x4 = x2 + (L2 / L1) * ((x1 - x2) * Math.cos(angle) - (y1 - y2) * Math.sin(angle));
const y4 = y2 + (L2 / L1) * ((y1 - y2) * Math.cos(angle) + (x1 - x2) * Math.sin(angle));
- this._points.push({ X: x1, Y: y1 });
- this._points.push({ X: x2, Y: y2 });
- this._points.push({ X: x3, Y: y3 });
- this._points.push({ X: x4, Y: y4 });
- this._points.push({ X: x2, Y: y2 });
+ points.push({ X: x1, Y: y1 });
+ points.push({ X: x2, Y: y2 });
+ points.push({ X: x3, Y: y3 });
+ points.push({ X: x4, Y: y4 });
+ points.push({ X: x2, Y: y2 });
}
break;
default:
}
- return false;
+ return points;
};
dispatchGesture = (gesture: Gestures, stroke?: InkData, text?: string) => {
@@ -389,7 +562,6 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
this._strokes.map((l, i) => {
const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; // this.getBounds(l, true);
return (
- // eslint-disable-next-line react/no-array-index-key
<svg key={i} width={b.width} height={b.height} style={{ top: 0, left: 0, transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: 'none', position: 'absolute', zIndex: 30000, overflow: 'visible' }}>
{InteractionUtils.CreatePolyline(
l,
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index a85a03aab..d7d8e9506 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/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 3920ecc2a..358274f0e 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -1,7 +1,9 @@
import { Bezier } from 'bezier-js';
+import * as fitCurve from 'fit-curve';
import * as _ from 'lodash';
import { action, makeObservable, observable, reaction, runInAction } from 'mobx';
import { Doc, NumListCast, Opt } from '../../fields/Doc';
+import { DocData } from '../../fields/DocSymbols';
import { InkData, InkField, InkTool } from '../../fields/InkField';
import { List } from '../../fields/List';
import { listSpec } from '../../fields/Schema';
@@ -9,7 +11,7 @@ import { Cast, NumCast } from '../../fields/Types';
import { PointData } from '../../pen-gestures/GestureTypes';
import { Point } from '../../pen-gestures/ndollar';
import { DocumentType } from '../documents/DocumentTypes';
-import { undoBatch } from '../util/UndoManager';
+import { undoable } from '../util/UndoManager';
import { FitOneCurve } from '../util/bezierFit';
import { InkingStroke } from './InkingStroke';
import { CollectionFreeFormView } from './collections/collectionFreeForm';
@@ -89,8 +91,7 @@ export class InkStrokeProperties {
* @param i index of first control point of segment being split
* @param control The list of all control points of the ink.
*/
- @undoBatch
- addPoints = (inkView: DocumentView, t: number, i: number, controls: { X: number; Y: number }[]) => {
+ addPoints = undoable((inkView: DocumentView, t: number, i: number, controls: { X: number; Y: number }[]) => {
this.applyFunction(inkView, (view: DocumentView /* , ink: InkData */) => {
const doc = view.Document;
const array = [controls[i], controls[i + 1], controls[i + 2], controls[i + 3]];
@@ -106,7 +107,7 @@ export class InkStrokeProperties {
return controls;
});
- };
+ }, 'add ink points');
/**
* Scales a handle point of a control point that is adjacent to a newly added one.
@@ -161,8 +162,7 @@ export class InkStrokeProperties {
/**
* Deletes the current control point of the selected ink instance.
*/
- @undoBatch
- deletePoints = (inkView: DocumentView, preserve: boolean) =>
+ deletePoints = undoable((inkView: DocumentView, preserve: boolean) => {
this.applyFunction(
inkView,
(view: DocumentView, ink: InkData) => {
@@ -201,6 +201,7 @@ export class InkStrokeProperties {
},
true
);
+ }, 'delete ink points');
/**
* Rotates ink stroke(s) about a point
@@ -208,8 +209,7 @@ export class InkStrokeProperties {
* @param angle The angle at which to rotate the ink in radians.
* @param scrpt The center point of the rotation in screen coordinates
*/
- @undoBatch
- rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: PointData) => {
+ rotateInk = undoable((inkStrokes: DocumentView[], angle: number, scrpt: PointData) => {
this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number /* , inkStrokeWidth: number */) => {
const inkCenterPt = view.ComponentView?.ptFromScreen?.(scrpt);
return !inkCenterPt
@@ -221,7 +221,7 @@ export class InkStrokeProperties {
return { X: newX + inkCenterPt.X, Y: newY + inkCenterPt.Y };
});
});
- };
+ }, 'rotate ink');
/**
* Rotates ink stroke(s) about a point
@@ -229,8 +229,7 @@ export class InkStrokeProperties {
* @param angle The angle at which to rotate the ink in radians.
* @param scrpt The center point of the rotation in screen coordinates
*/
- @undoBatch
- stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => {
+ stretchInk = undoable((inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => {
this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData) => {
const ptFromScreen = view.ComponentView?.ptFromScreen;
const ptToScreen = view.ComponentView?.ptToScreen;
@@ -244,77 +243,77 @@ export class InkStrokeProperties {
return ptFromScreen(newscrpt);
});
});
- };
+ }, 'stretch ink');
/**
* Handles the movement/scaling of a control point.
*/
- @undoBatch
- moveControlPtHandle = (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) =>
+ moveControlPtHandle = undoable((inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) => {
inkView &&
- this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
- const order = controlIndex % 4;
- const closed = InkingStroke.IsClosed(ink);
- const brokenIndices = Cast(inkView.Document.brokenInkIndices, listSpec('number'), []);
- if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1 && brokenIndices.findIndex(value => value === controlIndex) === -1) {
- const cptBefore = ink[controlIndex];
- const cpt = { X: cptBefore.X + deltaX, Y: cptBefore.Y + deltaY };
- const newink = origInk.slice();
- const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
- const splicedPoints = origInk.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
- const { nearestT, nearestSeg } = InkStrokeProperties.nearestPtToStroke(splicedPoints, cpt);
- if ((nearestSeg === 0 && nearestT < 1e-1) || (nearestSeg === 4 && 1 - nearestT < 1e-1) || nearestSeg < 0) return ink.slice();
- const samplesLeft: Point[] = [];
- const samplesRight: Point[] = [];
- let startDir = { x: 0, y: 0 };
- let endDir = { x: 0, y: 0 };
- for (let i = 0; i < nearestSeg / 4 + 1; i++) {
- const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
- if (i === 0) startDir = bez.derivative(_.isEqual(bez.derivative(0), { x: 0, y: 0, t: 0 }) ? 1e-8 : 0);
- if (i === nearestSeg / 4) endDir = bez.derivative(nearestT);
- for (let t = 0; t < (i === nearestSeg / 4 ? nearestT + 0.05 : 1); t += 0.05) {
- const pt = bez.compute(i !== nearestSeg / 4 ? t : Math.min(nearestT, t));
- samplesLeft.push(new Point(pt.x, pt.y));
+ this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+ const order = controlIndex % 4;
+ const closed = InkingStroke.IsClosed(ink);
+ const brokenIndices = Cast(inkView.Document.brokenInkIndices, listSpec('number'), []);
+ if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1 && brokenIndices.findIndex(value => value === controlIndex) === -1) {
+ const cptBefore = ink[controlIndex];
+ const cpt = { X: cptBefore.X + deltaX, Y: cptBefore.Y + deltaY };
+ const newink = origInk.slice();
+ const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
+ const splicedPoints = origInk.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
+ const { nearestT, nearestSeg } = InkStrokeProperties.nearestPtToStroke(splicedPoints, cpt);
+ if ((nearestSeg === 0 && nearestT < 1e-1) || (nearestSeg === 4 && 1 - nearestT < 1e-1) || nearestSeg < 0) return ink.slice();
+ const samplesLeft: Point[] = [];
+ const samplesRight: Point[] = [];
+ let startDir = { x: 0, y: 0 };
+ let endDir = { x: 0, y: 0 };
+ for (let i = 0; i < nearestSeg / 4 + 1; i++) {
+ const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+ if (i === 0) startDir = bez.derivative(_.isEqual(bez.derivative(0), { x: 0, y: 0, t: 0 }) ? 1e-8 : 0);
+ if (i === nearestSeg / 4) endDir = bez.derivative(nearestT);
+ for (let t = 0; t < (i === nearestSeg / 4 ? nearestT + 0.05 : 1); t += 0.05) {
+ const pt = bez.compute(i !== nearestSeg / 4 ? t : Math.min(nearestT, t));
+ samplesLeft.push(new Point(pt.x, pt.y));
+ }
}
- }
- let { finalCtrls } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
- for (let i = nearestSeg / 4; i < splicedPoints.length / 4; i++) {
- const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
- if (i === nearestSeg / 4) startDir = bez.derivative(nearestT);
- if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(_.isEqual(bez.derivative(1), { x: 0, y: 0, t: 1 }) ? 1 - 1e-8 : 1);
- for (let t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + 0.05 + 1e-7 : 1 + 1e-7); t += 0.05) {
- const pt = bez.compute(Math.min(1, t));
- samplesRight.push(new Point(pt.x, pt.y));
+ let { finalCtrls } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+ for (let i = nearestSeg / 4; i < splicedPoints.length / 4; i++) {
+ const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+ if (i === nearestSeg / 4) startDir = bez.derivative(nearestT);
+ if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(_.isEqual(bez.derivative(1), { x: 0, y: 0, t: 1 }) ? 1 - 1e-8 : 1);
+ for (let t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + 0.05 + 1e-7 : 1 + 1e-7); t += 0.05) {
+ const pt = bez.compute(Math.min(1, t));
+ samplesRight.push(new Point(pt.x, pt.y));
+ }
}
+ const { finalCtrls: rightCtrls /* , error: errorRight */ } = FitOneCurve(samplesRight, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+ finalCtrls = finalCtrls.concat(rightCtrls);
+ newink.splice(this._currentPoint - 4, 8, ...finalCtrls);
+ return newink;
}
- const { finalCtrls: rightCtrls /* , error: errorRight */ } = FitOneCurve(samplesRight, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
- finalCtrls = finalCtrls.concat(rightCtrls);
- newink.splice(this._currentPoint - 4, 8, ...finalCtrls);
- return newink;
- }
- return ink.map((pt, i) => {
- const leftHandlePoint = order === 0 && i === controlIndex + 1;
- const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2;
- if (controlIndex === i || (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || (order === 3 && i === controlIndex - 1)) {
- return { X: pt.X + deltaX, Y: pt.Y + deltaY };
- }
- if (
- controlIndex === i ||
- leftHandlePoint ||
- rightHandlePoint ||
- (order === 0 && controlIndex !== 0 && i === controlIndex - 1) ||
- ((order === 0 || order === 3) && (controlIndex === 0 || controlIndex === ink.length - 1) && (i === 1 || i === ink.length - 2) && closed) ||
- (order === 3 && i === controlIndex - 1) ||
- (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) ||
- (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) ||
- (ink[0].X === ink[ink.length - 1].X && ink[0].Y === ink[ink.length - 1].Y && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))
- ) {
- return { X: pt.X + deltaX, Y: pt.Y + deltaY };
- }
- return pt;
+ return ink.map((pt, i) => {
+ const leftHandlePoint = order === 0 && i === controlIndex + 1;
+ const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2;
+ if (controlIndex === i || (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || (order === 3 && i === controlIndex - 1)) {
+ return { X: pt.X + deltaX, Y: pt.Y + deltaY };
+ }
+ if (
+ controlIndex === i ||
+ leftHandlePoint ||
+ rightHandlePoint ||
+ (order === 0 && controlIndex !== 0 && i === controlIndex - 1) ||
+ ((order === 0 || order === 3) && (controlIndex === 0 || controlIndex === ink.length - 1) && (i === 1 || i === ink.length - 2) && closed) ||
+ (order === 3 && i === controlIndex - 1) ||
+ (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) ||
+ (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) ||
+ (ink[0].X === ink[ink.length - 1].X && ink[0].Y === ink[ink.length - 1].Y && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))
+ ) {
+ return { X: pt.X + deltaX, Y: pt.Y + deltaY };
+ }
+ return pt;
+ });
});
- });
+ }, 'move ink ctrl pt');
public static nearestPtToStroke(ctrlPoints: { X: number; Y: number }[], refInkSpacePt: { X: number; Y: number }, excludeSegs?: number[]) {
let distance = Number.MAX_SAFE_INTEGER;
@@ -322,7 +321,6 @@ export class InkStrokeProperties {
let nearestSeg = -1;
let nearestPt = { X: 0, Y: 0 };
for (let i = 0; i < ctrlPoints.length - 3; i += 4) {
- // eslint-disable-next-line no-continue
if (excludeSegs?.includes(i)) continue;
const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]];
const point = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).project({ x: refInkSpacePt.X, y: refInkSpacePt.Y });
@@ -467,8 +465,7 @@ export class InkStrokeProperties {
/**
* Handles the movement/scaling of a handle point.
*/
- @undoBatch
- moveTangentHandle = (inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
+ moveTangentHandle = undoable((inkView: DocumentView, deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => {
this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
const doc = view.Document;
const closed = InkingStroke.IsClosed(ink);
@@ -487,4 +484,37 @@ export class InkStrokeProperties {
}
return inkCopy;
});
+ }, 'move ink tangent');
+
+ sampleBezier = (curves: InkData) => {
+ const polylinePoints = [{ x: curves[0].X, y: curves[0].Y }];
+ for (let i = 0; i < curves.length / 4; i++) {
+ const bez = new Bezier(curves.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+ for (let t = 0.05; t < 1; t += 0.05) {
+ polylinePoints.push(bez.compute(t));
+ }
+ polylinePoints.push(bez.points[3]);
+ }
+ return polylinePoints.length > 2 ? polylinePoints : undefined;
+ };
+ /**
+ * Function that "smooths" ink strokes by sampling the curve, then fitting it with new bezier curves, subject to a
+ * maximum pixel error tolerance
+ * @param inkDocs
+ * @param tolerance how many pixels of error are allowed
+ */
+ smoothInkStrokes = undoable((inkDocs: Doc[], tolerance = 5) => {
+ inkDocs.forEach(inkDoc => {
+ const inkView = DocumentView.getDocumentView(inkDoc);
+ const inkStroke = inkView?.ComponentView as InkingStroke;
+ const polylinePoints = this.sampleBezier(inkStroke?.inkScaledData().inkData ?? [])?.map(pt => [pt.x, pt.y]);
+ if (polylinePoints) {
+ inkDoc[DocData].stroke = new InkField(
+ fitCurve.default(polylinePoints, tolerance)
+ .reduce((cpts, bez) =>
+ ({n: cpts.push(...bez.map(cpt => ({X:cpt[0], Y:cpt[1]}))), cpts}).cpts,
+ [] as {X:number, Y:number}[])); // prettier-ignore
+ }
+ });
+ }, 'smooth ink stroke');
}
diff --git a/src/client/views/InkTranscription.scss b/src/client/views/InkTranscription.scss
index bbb0a1afa..c77117ccc 100644
--- a/src/client/views/InkTranscription.scss
+++ b/src/client/views/InkTranscription.scss
@@ -2,4 +2,7 @@
.error-msg {
display: none !important;
}
-} \ No newline at end of file
+ .ms-editor {
+ top: 1000px;
+ }
+}
diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx
index 33db72960..24d53a8c8 100644
--- a/src/client/views/InkTranscription.tsx
+++ b/src/client/views/InkTranscription.tsx
@@ -1,350 +1,410 @@
-// 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.text = text; // transcription text
-// this.currGroup.icon_fieldKey = 'transcription'; // use the transcription icon template when iconifying
-// 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 } from '../../fields/Types';
+import { aggregateBounds } from '../../Utils';
+import { DocumentType } from '../documents/DocumentTypes';
+import { CollectionFreeFormView, MarqueeView } from './collections/collectionFreeForm';
+import { InkingStroke } from './InkingStroke';
+import './InkTranscription.scss';
+import { Docs } from '../documents/Documents';
+import { DocumentView } from './nodes/DocumentView';
+import { ImageField } from '../../fields/URLField';
+import { gptHandwriting } from '../apis/gpt/GPT';
+import { URLField } from '../../fields/URLField';
+/**
+ * Class component that handles inking in writing mode
+ */
+export class InkTranscription extends React.Component {
+ // eslint-disable-next-line no-use-before-define
+ static Instance: InkTranscription;
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ @observable _mathRegister: any = undefined;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ @observable _mathRef: any = undefined;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ @observable _textRegister: any = undefined;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ @observable _textRef: any = undefined;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ @observable iinkEditor: any = undefined;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ private lastJiix: any;
+ private currGroup?: Doc;
+ private collectionFreeForm?: CollectionFreeFormView;
+
+ constructor(props: Readonly<object>) {
+ super(props);
+
+ InkTranscription.Instance = this;
+ }
+ @action
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ setMathRef = async (r: any) => {
+ if (!this._textRegister && r) {
+ 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',
+ },
+ },
+ };
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const editor = new iink.Editor(r, options as any);
+
+ await editor.initialize();
+
+ this._textRegister = r;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
+
+ return (this._textRef = r);
+ }
+ };
+ @action
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ setTextRef = async (r: any) => {
+ if (!this._textRegister && r) {
+ 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',
+ },
+ },
+ };
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const editor = new iink.Editor(r, options as any);
+
+ await editor.initialize();
+ this.iinkEditor = editor;
+ this._textRegister = r;
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ r?.addEventListener('exported', (e: any) => this.exportInk(e, this._textRef));
+
+ return (this._textRef = r);
+ }
+ };
+
+ /**
+ * 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());
+ });
+ this.currGroup = groupDoc;
+ const pointerData = strokes.map((stroke, i) => this.inkJSON(stroke, times[i]));
+ if (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(',');
+ }
+ 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;
+ }
+ const strokeObjects: strokeData[] = [];
+ stroke.forEach(point => {
+ const 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
+ */
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ exportInk = async (e: any, ref: any) => {
+ const exports = e.detail['application/vnd.myscript.jiix'];
+ if (exports) {
+ 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>();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ this.lastJiix.words.map((word: any) => {
+ if (word.items) {
+ word.items.forEach((i: { id: string; timestamp: string; X: Array<number>; Y: Array<number>; F: Array<number> }) => {
+ 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?.();
+ const image = await this.getIcon();
+ 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);
+ } catch {
+ console.error('Error getting image');
+ }
+ const textBoxText = 'iink: ' + text + '\n' + '\n' + 'ChatGPT: ' + response;
+ this.currGroup.transcription = response;
+ this.currGroup.title = response;
+ if (!this.currGroup.hasTextBox) {
+ const newDoc = Docs.Create.TextDocument(textBoxText, { title: '', x: this.currGroup.x as number, y: (this.currGroup.y as number) + (this.currGroup.height as number) });
+ newDoc.height = 200;
+ this.collectionFreeForm?.addDocument(newDoc);
+ this.currGroup.hasTextBox = true;
+ }
+ ref.editor.clear();
+ }
+ }
+ }
+ };
+ /**
+ * gets the icon of the collection that was just made
+ * @returns the image of the collection
+ */
+ async getIcon() {
+ const docView = DocumentView.getDocumentView(this.currGroup);
+ if (docView) {
+ docView.ComponentView?.updateIcon?.();
+ return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+ }
+ return undefined;
+ }
+ /**
+ * converts the image to base url formate
+ * @param imageUrl imageurl taken from the collection icon
+ */
+ imageUrlToBase64 = async (imageUrl: string): Promise<string> => {
+ try {
+ const response = await fetch(imageUrl);
+ 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.forEach(
+ 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.forEach(
+ 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 = MarqueeView.getCollection(selected, undefined, true, marqViewRef?.Bounds ?? { top: 1, left: 1, width: 1, height: 1 });
+ // 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
+ docView.props.addDocument?.(newCollection);
+ newCollection.hasTextBox = false;
+ return newCollection;
+ }
+
+ render() {
+ return (
+ <div className="ink-transcription">
+ <div className="math-editor" ref={this.setMathRef}></div>
+ <div className="text-editor" ref={this.setTextRef}></div>
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 498042938..270266a94 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -30,7 +30,6 @@ import { InkData, InkField } from '../../fields/InkField';
import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types';
import { TraceMobx } from '../../fields/util';
import { Gestures } from '../../pen-gestures/GestureTypes';
-import { CognitiveServices } from '../cognitive_services/CognitiveServices';
import { Docs } from '../documents/Documents';
import { DocumentType } from '../documents/DocumentTypes';
import { InteractionUtils } from '../util/InteractionUtils';
@@ -48,8 +47,11 @@ import { FormattedTextBox, FormattedTextBoxProps } from './nodes/formattedText/F
import { PinDocView, PinProps } from './PinFuncs';
import { StyleProp } from './StyleProp';
import { ViewBoxInterface } from './ViewBoxInterface';
+import { InkTranscription } from './InkTranscription';
+import { CollectionFreeFormView } from './collections/collectionFreeForm';
+import { DocumentView } from './nodes/DocumentView';
-// eslint-disable-next-line @typescript-eslint/no-var-requires
+// eslint-disable-next-line @typescript-eslint/no-require-imports
const { INK_MASK_SIZE } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
@observer
@@ -107,10 +109,19 @@ 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 ?? [];
- CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ['inkAnalysis', 'handwriting'], [data]);
- }
+ analyzeStrokes = () => {
+ const ffView = CollectionFreeFormView.from(this.DocumentView?.());
+ if (ffView) {
+ const selected = DocumentView.SelectedDocs();
+ const newCollection = InkTranscription.Instance.groupInkDocs(
+ selected.filter(doc => doc.embedContainer),
+ ffView
+ );
+ ffView.unprocessedDocs = [];
+
+ InkTranscription.Instance.transcribeInk(newCollection, selected, false);
+ }
+ };
/**
* Toggles whether the ink stroke is displayed as an overlay mask or as a regular stroke.
@@ -462,7 +473,6 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
// mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset',
cursor: this._props.isSelected() ? 'default' : undefined,
}}
- // eslint-disable-next-line react/jsx-props-no-spreading
{...interactions}>
{clickableLine(this.onPointerDown, isInkMask)}
{isInkMask ? null : inkLine}
@@ -479,7 +489,6 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>()
// top: (this._props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this._props.NativeDimScaling?.() || 1)) / 2,
}}>
<FormattedTextBox
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
setHeight={undefined}
setContentViewBox={this.setSubContentView} // this makes the inkingStroke the "dominant" component - ie, it will show the inking UI when selected (not text)
diff --git a/src/client/views/LightboxView.scss b/src/client/views/LightboxView.scss
index 6da5c0338..3e65843df 100644
--- a/src/client/views/LightboxView.scss
+++ b/src/client/views/LightboxView.scss
@@ -1,7 +1,7 @@
.lightboxView-navBtn {
margin: auto;
position: absolute;
- right: 10;
+ right: 19;
top: 10;
background: transparent;
border-radius: 8;
@@ -16,7 +16,7 @@
.lightboxView-tabBtn {
margin: auto;
position: absolute;
- right: 45;
+ right: 54;
top: 10;
background: transparent;
border-radius: 8;
@@ -28,10 +28,26 @@
opacity: 1;
}
}
+.lightboxView-paletteBtn {
+ margin: auto;
+ position: absolute;
+ right: 89;
+ top: 10;
+ background: transparent;
+ border-radius: 8;
+ opacity: 0.7;
+ width: 25;
+ flex-direction: column;
+ display: flex;
+ &:hover {
+ opacity: 1;
+ }
+}
+
.lightboxView-penBtn {
margin: auto;
position: absolute;
- right: 80;
+ right: 124;
top: 10;
background: transparent;
border-radius: 8;
@@ -46,7 +62,7 @@
.lightboxView-exploreBtn {
margin: auto;
position: absolute;
- right: 115;
+ right: 159;
top: 10;
background: transparent;
border-radius: 8;
diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx
index b8b73e7dd..a543b4875 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -10,7 +10,7 @@ import { emptyFunction } from '../../Utils';
import { CreateLinkToActiveAudio, Doc, DocListCast, FieldResult, Opt, returnEmptyDoclist } from '../../fields/Doc';
import { Id } from '../../fields/FieldSymbols';
import { InkTool } from '../../fields/InkField';
-import { BoolCast, Cast, NumCast, toList } from '../../fields/Types';
+import { BoolCast, Cast, DocCast, NumCast, toList } from '../../fields/Types';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
import { SnappingManager } from '../util/SnappingManager';
import { Transform } from '../util/Transform';
@@ -21,6 +21,7 @@ import { OverlayView } from './OverlayView';
import { DefaultStyleProvider, returnEmptyDocViewList, wavyBorderPath } from './StyleProvider';
import { DocumentView } from './nodes/DocumentView';
import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
+import { AnnotationPalette } from './smartdraw/AnnotationPalette';
interface LightboxViewProps {
PanelWidth: number;
@@ -34,13 +35,17 @@ type LightboxSavedState = { [key: string]: FieldResult; }; // prettier-ignore
@observer
export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
/**
- * Determines whether a DocumentView is descendant of the lightbox view
+ * Determines whether a DocumentView is descendant of the lightbox view (or any of its pop-ups like the annotationPalette)
* @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)) || undefined
+ );
+ } // prettier-ignore
public static LightboxDoc = () => LightboxView.Instance?._doc;
- // eslint-disable-next-line no-use-before-define
static Instance: LightboxView;
private _path: {
doc: Opt<Doc>; //
@@ -51,12 +56,14 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
}[] = [];
private _savedState: LightboxSavedState = {};
private _history: { doc: Doc; target?: Doc }[] = [];
+ private _annoPaletteView: AnnotationPalette | null = null;
@observable private _future: Doc[] = [];
@observable private _layoutTemplate: Opt<Doc> = undefined;
@observable private _layoutTemplateString: Opt<string> = undefined;
@observable private _doc: Opt<Doc> = undefined;
@observable private _docTarget: Opt<Doc> = undefined;
@observable private _docView: Opt<DocumentView> = undefined;
+ @observable private _showPalette: boolean = false;
@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
@@ -69,6 +76,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
@@ -101,6 +109,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
this._history = [];
Doc.ActiveTool = InkTool.None;
SnappingManager.SetExploreMode(false);
+ this._showPalette = false;
}
DocumentView.DeselectAll();
if (future) {
@@ -200,6 +209,10 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
toggleFitWidth = () => {
this._doc && (this._doc._layout_fitWidth = !this._doc._layout_fitWidth);
};
+ togglePalette = () => {
+ this._showPalette = !this._showPalette;
+ // if (this._showPalette === false) AnnotationPalette.Instance.resetPalette(true);
+ };
togglePen = () => {
Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
};
@@ -304,6 +317,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 ? true : false, this.previous)}
{this.renderNavBtn(
this._props.PanelWidth - Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]),
@@ -316,7 +330,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', BoolCast(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>
@@ -325,7 +340,6 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
}
interface LightboxTourBtnProps {
navBtn: (left: Opt<string | number>, bottom: Opt<number>, top: number, icon: IconProp, display: boolean, click: () => void, color?: string) => JSX.Element;
- // eslint-disable-next-line react/no-unused-prop-types
future: () => Opt<Doc[]>;
stepInto: () => void;
lightboxDoc: () => Opt<Doc>;
diff --git a/src/client/views/Main.tsx b/src/client/views/Main.tsx
index f7cd0e925..73d2872d1 100644
--- a/src/client/views/Main.tsx
+++ b/src/client/views/Main.tsx
@@ -1,9 +1,10 @@
-/* eslint-disable no-new */
// if ((module as any).hot) {
// (module as any).hot.accept();
// }
import * as dotenv from 'dotenv'; // see https://github.com/motdotla/dotenv#how-do-i-use-dotenv-with-import
+import { Node } from 'prosemirror-model';
+import { EditorView } from 'prosemirror-view';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import { AssignAllExtensions } from '../../extensions/Extensions';
@@ -23,6 +24,8 @@ import { CollectionView } from './collections/CollectionView';
import { TabDocView } from './collections/TabDocView';
import { CollectionFreeFormView } from './collections/collectionFreeForm';
import { CollectionFreeFormInfoUI } from './collections/collectionFreeForm/CollectionFreeFormInfoUI';
+import { FaceCollectionBox, UniqueFaceBox } from './collections/collectionFreeForm/FaceCollectionBox';
+import { ImageLabelBox } from './collections/collectionFreeForm/ImageLabelBox';
import { CollectionSchemaView } from './collections/collectionSchema/CollectionSchemaView';
import { SchemaRowBox } from './collections/collectionSchema/SchemaRowBox';
import './global/globalScripts';
@@ -60,12 +63,9 @@ import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import { SummaryView } from './nodes/formattedText/SummaryView';
import { ImportElementBox } from './nodes/importBox/ImportElementBox';
import { PresBox, PresElementBox } from './nodes/trails';
-import { SearchBox } from './search/SearchBox';
-import { ImageLabelBox } from './collections/collectionFreeForm/ImageLabelBox';
import { FaceRecognitionHandler } from './search/FaceRecognitionHandler';
-import { FaceCollectionBox, UniqueFaceBox } from './collections/collectionFreeForm/FaceCollectionBox';
-import { Node } from 'prosemirror-model';
-import { EditorView } from 'prosemirror-view';
+import { SearchBox } from './search/SearchBox';
+import { AnnotationPalette } from './smartdraw/AnnotationPalette';
dotenv.config();
@@ -118,6 +118,7 @@ FieldLoader.ServerLoadStatus = { requested: 0, retrieved: 0, message: 'cache' };
KeyValueBox.Init();
PresBox.Init(TabDocView.AllTabDocs);
DocumentContentsView.Init(KeyValueBox.LayoutString(), {
+ AnnotationPalette,
FormattedTextBox,
ImageBox,
FontIconBox,
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index c852f9a2a..76c67a252 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -3,7 +3,7 @@ import { faBuffer, faHireAHelper } from '@fortawesome/free-brands-svg-icons';
import * as far from '@fortawesome/free-regular-svg-icons';
import * as fa from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { action, computed, configure, makeObservable, observable, reaction, runInAction, trace } from 'mobx';
+import { action, computed, configure, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import ResizeObserver from 'resize-observer-polyfill';
@@ -74,8 +74,10 @@ 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 { InkTranscription } from './InkTranscription';
-// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports
+// eslint-disable-next-line @typescript-eslint/no-require-imports
const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
@observer
@@ -315,6 +317,7 @@ export class MainView extends ObservableReactComponent<object> {
fa.faCompass,
fa.faSnowflake,
fa.faStar,
+ fa.faSplotch,
fa.faMicrophone,
fa.faCircleHalfStroke,
fa.faKeyboard,
@@ -337,6 +340,7 @@ export class MainView extends ObservableReactComponent<object> {
fa.faTerminal,
fa.faToggleOn,
fa.faFile,
+ fa.faFileExport,
fa.faLocationArrow,
fa.faSearch,
fa.faFileDownload,
@@ -376,6 +380,7 @@ export class MainView extends ObservableReactComponent<object> {
fa.faXmark,
fa.faExclamation,
fa.faFileAlt,
+ fa.faFileArrowDown,
fa.faFileAudio,
fa.faFileVideo,
fa.faFilePdf,
@@ -392,6 +397,7 @@ export class MainView extends ObservableReactComponent<object> {
fa.faArrowsLeftRight,
fa.faPause,
fa.faPen,
+ fa.faUserPen,
fa.faPenNib,
fa.faPhone,
fa.faPlay,
@@ -402,6 +408,7 @@ export class MainView extends ObservableReactComponent<object> {
fa.faArrowsAltV,
fa.faTimesCircle,
fa.faThumbtack,
+ fa.faScissors,
fa.faTree,
fa.faTv,
fa.faUndoAlt,
@@ -429,6 +436,7 @@ export class MainView extends ObservableReactComponent<object> {
fa.faBold,
fa.faItalic,
fa.faClipboard,
+ fa.faClipboardCheck,
fa.faUnderline,
fa.faStrikethrough,
fa.faSuperscript,
@@ -437,6 +445,7 @@ export class MainView extends ObservableReactComponent<object> {
fa.faEyeDropper,
fa.faPaintRoller,
fa.faBars,
+ fa.faBarsStaggered,
fa.faBrush,
fa.faShapes,
fa.faEllipsisH,
@@ -475,6 +484,8 @@ export class MainView extends ObservableReactComponent<object> {
fa.faHashtag,
fa.faAlignJustify,
fa.faCheckSquare,
+ fa.faSquarePlus,
+ fa.faReply,
fa.faListUl,
fa.faWindowMinimize,
fa.faWindowRestore,
@@ -574,6 +585,7 @@ export class MainView extends ObservableReactComponent<object> {
DocumentManager.removeOverlayViews();
Doc.linkFollowUnhighlight();
const targets = document.elementsFromPoint(e.x, e.y);
+ const targetClasses = targets.map(target => target.className.toString());
if (targets.length) {
let targClass = targets[0].className.toString();
for (let i = 0; i < targets.length - 1; i++) {
@@ -581,6 +593,8 @@ export class MainView extends ObservableReactComponent<object> {
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();
}
});
@@ -838,7 +852,6 @@ export class MainView extends ObservableReactComponent<object> {
};
@computed get mainInnerContent() {
- trace();
const leftMenuFlyoutWidth = this._leftMenuFlyoutWidth + this.leftMenuWidth();
const width = this.propertiesWidth() + leftMenuFlyoutWidth;
return (
@@ -966,13 +979,11 @@ export class MainView extends ObservableReactComponent<object> {
<div className="mainView-snapLines">
<svg style={{ width: '100%', height: '100%' }}>
{[
- ...SnappingManager.HorizSnapLines.map((l, i) => (
- // eslint-disable-next-line react/no-array-index-key
- <line key={'horiz' + i} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
+ ...SnappingManager.HorizSnapLines.map(l => (
+ <line key={'horiz' + l} x1="0" y1={l} x2="2000" y2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
)),
- ...SnappingManager.VertSnapLines.map((l, i) => (
- // eslint-disable-next-line react/no-array-index-key
- <line key={'vert' + i} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
+ ...SnappingManager.VertSnapLines.map(l => (
+ <line key={'vert' + l} y1={this.topOfMainDocContent.toString()} x1={l} y2="2000" x2={l} stroke={lightOrDark(StrCast(dragPar.layoutDoc.backgroundColor, 'gray'))} opacity={0.3} strokeWidth={1} strokeDasharray="2 2" />
)),
]}
</svg>
@@ -1060,10 +1071,7 @@ export class MainView extends ObservableReactComponent<object> {
docView={DocButtonState.Instance.LinkEditorDocView}
/>
) : null}
- {LinkInfo.Instance?.LinkInfo ? (
- // eslint-disable-next-line react/jsx-props-no-spreading
- <LinkDocPreview {...LinkInfo.Instance.LinkInfo} />
- ) : null}
+ {LinkInfo.Instance?.LinkInfo ? <LinkDocPreview {...LinkInfo.Instance.LinkInfo} /> : null}
{((page: string) => {
// prettier-ignore
switch (page) {
@@ -1081,6 +1089,7 @@ export class MainView extends ObservableReactComponent<object> {
<TaskCompletionBox />
<ContextMenu />
<ImageLabelHandler />
+ <SmartDrawHandler />
<AnchorMenu />
<MapAnchorMenu />
<DirectionsAnchorMenu />
@@ -1088,6 +1097,7 @@ export class MainView extends ObservableReactComponent<object> {
<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/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index 024ae7ba8..7266875c5 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -27,7 +27,7 @@ export interface MarqueeAnnotatorProps {
containerOffset?: () => number[];
marqueeContainer: HTMLDivElement;
docView: () => DocumentView;
- savedAnnotations: () => ObservableMap<number, (HTMLDivElement& { marqueeing?: boolean})[]>;
+ savedAnnotations: () => ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>;
selectionText: () => string;
annotationLayer: HTMLDivElement;
addDocument: (doc: Doc) => boolean;
@@ -61,7 +61,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
});
@undoBatch
- makeAnnotationDocument = (color: string, isLinkButton?: boolean, savedAnnotations?: ObservableMap<number, (HTMLDivElement& { marqueeing?: boolean})[]>): Opt<Doc> => {
+ makeAnnotationDocument = (color: string, isLinkButton?: boolean, savedAnnotations?: ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>): Opt<Doc> => {
const savedAnnoMap = savedAnnotations?.values() && Array.from(savedAnnotations?.values()).length ? savedAnnotations : this.props.savedAnnotations();
if (savedAnnoMap.size === 0) return undefined;
const savedAnnos = Array.from(savedAnnoMap.values())[0];
@@ -138,7 +138,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
return annotationDoc as Doc;
};
- public static previewNewAnnotation = action((savedAnnotations: ObservableMap<number, (HTMLDivElement& { marqueeing?: boolean})[]>, annotationLayer: HTMLDivElement & { marqueeing?: boolean}, div: HTMLDivElement, page: number) => {
+ public static previewNewAnnotation = action((savedAnnotations: ObservableMap<number, (HTMLDivElement & { marqueeing?: boolean })[]>, annotationLayer: HTMLDivElement & { marqueeing?: boolean }, div: HTMLDivElement, page: number) => {
div.style.backgroundColor = '#ACCEF7';
div.style.opacity = '0.5';
annotationLayer.append(div);
@@ -266,7 +266,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
if (!this.isEmpty && marqueeStyle) {
// configure and show the annotation/link menu if a the drag region is big enough
// copy the temporary marquee to allow for multiple selections (not currently available though).
- const copy: (HTMLDivElement & {marqueeing?: boolean}) = document.createElement('div');
+ const copy: HTMLDivElement & { marqueeing?: boolean } = document.createElement('div');
const scale = (this.props.scaling?.() || 1) * NumCast(this.props.Document._freeform_scale, 1);
['border', 'opacity', 'top', 'left', 'width', 'height'].forEach(prop => {
copy.style[prop as unknown as number] = marqueeStyle[prop as unknown as number]; // bcz: hack to get around TS type checking for array index with strings
diff --git a/src/client/views/PropertiesView.scss b/src/client/views/PropertiesView.scss
index 840df41e7..a5e60b831 100644
--- a/src/client/views/PropertiesView.scss
+++ b/src/client/views/PropertiesView.scss
@@ -638,3 +638,9 @@
padding-left: 8px;
background-color: rgb(51, 51, 51);
}
+
+.smooth,
+.color,
+.smooth-slider {
+ margin-top: 7px;
+}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index b9f928cba..71d184497 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -2,7 +2,7 @@ import { IconLookup, IconProp } from '@fortawesome/fontawesome-svg-core';
import { faAnchor, faArrowRight, faWindowMaximize } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Checkbox, Tooltip } from '@mui/material';
-import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Type } from 'browndash-components';
+import { Colors, EditableText, IconButton, NumberInput, Size, Slider, Toggle, ToggleType, Type } from 'browndash-components';
import { concat } from 'lodash';
import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
@@ -12,7 +12,7 @@ import * as Icons from 'react-icons/bs'; // {BsCollectionFill, BsFillFileEarmark
import ResizeObserver from 'resize-observer-polyfill';
import { ClientUtils, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, setupMoveUpEvents } from '../../ClientUtils';
import { emptyFunction } from '../../Utils';
-import { Doc, Field, FieldResult, FieldType, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast, returnEmptyDoclist } from '../../fields/Doc';
+import { Doc, DocListCast, Field, FieldResult, FieldType, HierarchyMapping, NumListCast, Opt, ReverseHierarchyMap, StrListCast, returnEmptyDoclist } from '../../fields/Doc';
import { AclAdmin, DocAcl, DocData } from '../../fields/DocSymbols';
import { Id } from '../../fields/FieldSymbols';
import { InkField } from '../../fields/InkField';
@@ -23,6 +23,7 @@ import { GetEffectiveAcl, SharingPermissions, normalizeEmail } from '../../field
import { CollectionViewType, DocumentType } from '../documents/DocumentTypes';
import { GroupManager } from '../util/GroupManager';
import { LinkManager } from '../util/LinkManager';
+import { SettingsManager } from '../util/SettingsManager';
import { SharingManager } from '../util/SharingManager';
import { SnappingManager } from '../util/SnappingManager';
import { Transform } from '../util/Transform';
@@ -30,6 +31,7 @@ import { UndoManager, undoBatch, undoable } from '../util/UndoManager';
import { EditableView } from './EditableView';
import { FilterPanel } from './FilterPanel';
import { InkStrokeProperties } from './InkStrokeProperties';
+import { InkingStroke } from './InkingStroke';
import { ObservableReactComponent } from './ObservableReactComponent';
import { PropertiesButtons } from './PropertiesButtons';
import { PropertiesDocBacklinksSelector } from './PropertiesDocBacklinksSelector';
@@ -41,6 +43,7 @@ import { DocumentView } from './nodes/DocumentView';
import { StyleProviderFuncType } from './nodes/FieldView';
import { OpenWhere } from './nodes/OpenWhere';
import { PresBox, PresEffect, PresEffectDirection } from './nodes/trails';
+import { SmartDrawHandler } from './smartdraw/SmartDrawHandler';
interface PropertiesViewProps {
width: number;
@@ -71,6 +74,10 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
return 200;
}
+ @computed get containsInkDoc() {
+ return this.containsInk(this.selectedDoc);
+ }
+
@computed get selectedDoc() {
return DocumentView.SelectedSchemaDoc() || this.selectedDocumentView?.Document || Doc.ActiveDashboard;
}
@@ -807,6 +814,9 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
return Field.toString(this.selectedDoc?.[DocData][key] as FieldType);
}
+ @computed get selectedStrokes() {
+ return this.containsInkDoc ? DocListCast(this.selectedDoc[DocData].data) : DocumentView.SelectedSchemaDoc() ? [DocumentView.SelectedSchemaDoc()!] : DocumentView.SelectedDocs().filter(doc => doc.layout_isSvg);
+ }
@computed get shapeXps() { return NumCast(this.selectedDoc?.x); } // prettier-ignore
set shapeXps(value) { this.selectedDoc && (this.selectedDoc.x = Math.round(value * 100) / 100); } // prettier-ignore
@computed get shapeYps() { return NumCast(this.selectedDoc?.y); } // prettier-ignore
@@ -815,8 +825,12 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
set shapeWid(value) { this.selectedDoc && (this.selectedDoc._width = Math.round(value * 100) / 100); } // prettier-ignore
@computed get shapeHgt() { return NumCast(this.selectedDoc?._height); } // prettier-ignore
set shapeHgt(value) { this.selectedDoc && (this.selectedDoc._height = Math.round(value * 100) / 100); } // prettier-ignore
- @computed get strokeThk(){ return NumCast(this.selectedDoc?.[DocData].stroke_width); } // prettier-ignore
- set strokeThk(value) { this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Math.round(value * 100) / 100); } // prettier-ignore
+ @computed get strokeThk(){ return NumCast(this.selectedStrokes.lastElement()?.[DocData].stroke_width); } // prettier-ignore
+ set strokeThk(value) {
+ this.selectedStrokes.forEach(doc => {
+ doc[DocData].stroke_width = Math.round(value * 100) / 100;
+ });
+ }
@computed get hgtInput() {
return this.inputBoxDuo(
@@ -853,10 +867,22 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
private _lastDash: string = '2';
- @computed get colorFil() { return StrCast(this.selectedDoc?.[DocData].fillColor); } // prettier-ignore
- set colorFil(value) { this.selectedDoc && (this.selectedDoc[DocData].fillColor = value || undefined); } // prettier-ignore
- @computed get colorStk() { return StrCast(this.selectedDoc?.[DocData].color); } // prettier-ignore
- set colorStk(value) { this.selectedDoc && (this.selectedDoc[DocData].color = value || undefined); } // prettier-ignore
+ @computed get colorFil() { return StrCast(this.selectedStrokes.lastElement()?.[DocData].fillColor); } // prettier-ignore
+ set colorFil(value) {
+ this.selectedStrokes.forEach(doc => {
+ const inkStroke = DocumentView.getDocumentView(doc)?.ComponentView as InkingStroke;
+ const { inkData } = inkStroke.inkScaledData();
+ if (InkingStroke.IsClosed(inkData)) {
+ doc[DocData].fillColor = value || undefined;
+ }
+ });
+ }
+ @computed get colorStk() { return StrCast(this.selectedStrokes.lastElement()?.[DocData].color); } // prettier-ignore
+ set colorStk(value) {
+ this.selectedStrokes.forEach(doc => {
+ doc[DocData].color = value || undefined;
+ });
+ }
colorButton(value: string, type: string, setter: () => void) {
return (
@@ -927,26 +953,93 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
);
}
- @computed get dashdStk() { return StrCast(this.selectedDoc?.stroke_dash); } // prettier-ignore
+ @computed get smoothAndColor() {
+ const targetDoc = this.selectedLayoutDoc;
+ const smoothNumber = this.getNumber(
+ 'Smooth Amount',
+ '',
+ 1,
+ Math.max(10, this.smoothAmt),
+ this.smoothAmt,
+ (val: number) => {
+ !isNaN(val) && (this.smoothAmt = val);
+ },
+ 10,
+ 1
+ );
+ return (
+ <div>
+ {!targetDoc.layout_isSvg && this.containsInkDoc && (
+ <div className="color">
+ <Toggle
+ text={'Color with GPT'}
+ color={SettingsManager.userColor}
+ icon={<FontAwesomeIcon icon="fill-drip" />}
+ iconPlacement="left"
+ align="flex-start"
+ fillWidth
+ toggleType={ToggleType.BUTTON}
+ onClick={undoable(() => {
+ SmartDrawHandler.Instance.colorWithGPT(targetDoc);
+ }, 'smoothStrokes')}
+ />
+ </div>
+ )}
+ <div className="smooth">
+ <Toggle
+ text={'Smooth Ink Strokes'}
+ color={SettingsManager.userColor}
+ icon={<FontAwesomeIcon icon="bezier-curve" />}
+ iconPlacement="left"
+ align="flex-start"
+ fillWidth
+ toggleType={ToggleType.BUTTON}
+ onClick={undoable(() => {
+ InkStrokeProperties.Instance.smoothInkStrokes(this.selectedStrokes, this.smoothAmt);
+ }, 'smoothStrokes')}
+ />
+ </div>
+ <div className="smooth-slider">{smoothNumber}</div>
+ </div>
+ );
+ }
+
+ @computed get dashdStk() { return this.selectedStrokes[0]?.stroke_dash || ''; } // prettier-ignore
set dashdStk(value) {
- value && (this._lastDash = value);
- this.selectedDoc && (this.selectedDoc[DocData].stroke_dash = value ? this._lastDash : undefined);
+ value && (this._lastDash = value as string);
+ this.selectedStrokes.forEach(doc => {
+ doc[DocData].stroke_dash = value ? this._lastDash : undefined;
+ });
}
@computed get widthStk() { return this.getField('stroke_width') || '1'; } // prettier-ignore
set widthStk(value) {
- this.selectedDoc && (this.selectedDoc[DocData].stroke_width = Number(value));
+ this.selectedStrokes.forEach(doc => {
+ doc[DocData].stroke_width = Number(value);
+ });
}
@computed get markScal() { return Number(this.getField('stroke_markerScale') || '1'); } // prettier-ignore
set markScal(value) {
- this.selectedDoc && (this.selectedDoc[DocData].stroke_markerScale = Number(value));
+ this.selectedStrokes.forEach(doc => {
+ doc[DocData].stroke_markerScale = Number(value);
+ });
+ }
+ @computed get smoothAmt() { return Number(this.getField('stroke_smoothAmount') || '5'); } // prettier-ignore
+ set smoothAmt(value) {
+ this.selectedStrokes.forEach(doc => {
+ doc[DocData].stroke_smoothAmount = Number(value);
+ });
}
@computed get markHead() { return this.getField('stroke_startMarker') || ''; } // prettier-ignore
set markHead(value) {
- this.selectedDoc && (this.selectedDoc[DocData].stroke_startMarker = value);
+ this.selectedStrokes.forEach(doc => {
+ doc[DocData].stroke_startMarker = value;
+ });
}
@computed get markTail() { return this.getField('stroke_endMarker') || ''; } // prettier-ignore
set markTail(value) {
- this.selectedDoc && (this.selectedDoc[DocData].stroke_endMarker = value);
+ this.selectedStrokes.forEach(doc => {
+ doc[DocData].stroke_endMarker = value;
+ });
}
regInput = (key: string, value: string | number | undefined, setter: (val: string) => void) => (
@@ -1053,44 +1146,51 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
this.dashdStk = this.dashdStk === '2' ? '0' : '2';
};
- @computed get appearanceEditor() {
+ @computed get inkEditor() {
return (
- <div className="appearance-editor">
+ <div className="ink-editor">
{this.widthAndDash}
{this.strokeAndFill}
+ {this.smoothAndColor}
</div>
);
}
_sliderBatch: UndoManager.Batch | undefined;
+ _sliderKey = '';
setFinalNumber = () => {
+ this._sliderKey = '';
this._sliderBatch?.end();
};
- getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: (val: number) => void, autorange?: number, autorangeMinVal?: number) => (
- <div key={label + (this.selectedDoc?.title ?? '')}>
- <NumberInput formLabel={label} formLabelPlacement="left" type={Type.SEC} unit={unit} fillWidth color={this.color} number={number} setNumber={setNumber} min={min} max={max} />
- <Slider
- key={label}
- onPointerDown={() => {
- this._sliderBatch = UndoManager.StartBatch('slider ' + label);
- }}
- multithumb={false}
- color={this.color}
- size={Size.XSMALL}
- min={min}
- max={max}
- autorangeMinVal={autorangeMinVal}
- autorange={autorange}
- number={number}
- unit={unit}
- decimals={1}
- setFinalNumber={this.setFinalNumber}
- setNumber={setNumber}
- fillWidth
- />
- </div>
- );
+ getNumber = (label: string, unit: string, min: number, max: number, number: number, setNumber: (val: number) => void, autorange?: number, autorangeMinVal?: number) => {
+ const key = this._sliderKey || label + min + max + number;
+ return (
+ <div key={label + (this.selectedDoc?.title ?? '')}>
+ <NumberInput formLabel={label} formLabelPlacement="left" type={Type.SEC} unit={unit} fillWidth color={this.color} number={number} setNumber={setNumber} min={min} max={max} />
+ <Slider
+ key={key}
+ onPointerDown={() => {
+ this._sliderKey = key;
+ this._sliderBatch = UndoManager.StartBatch('slider ' + label);
+ }}
+ multithumb={false}
+ color={this.color}
+ size={Size.XSMALL}
+ min={min}
+ max={max}
+ autorangeMinVal={autorangeMinVal}
+ autorange={autorange}
+ number={number}
+ unit={unit}
+ decimals={1}
+ setFinalNumber={this.setFinalNumber}
+ setNumber={setNumber}
+ fillWidth
+ />
+ </div>
+ );
+ };
setVal = (func: (doc: Doc, val: number) => void) => (val: number) => this.selectedDoc && !isNaN(val) && func(this.selectedDoc, val);
@computed get transformEditor() {
@@ -1177,29 +1277,41 @@ export class PropertiesView extends ObservableReactComponent<PropertiesViewProps
@computed get filtersSubMenu() {
return (
- // prettier-ignore
<PropertiesSection title="Filters" isOpen={this.openFilters} setIsOpen={action(bool => { this.openFilters = bool; })} onDoubleClick={this.CloseAll}>
<div className="propertiesView-content filters" style={{ position: 'relative', height: 'auto' }}>
<FilterPanel Document={this.selectedDoc ?? Doc.ActiveDashboard!} />
</div>
</PropertiesSection>
- );
+ ); // prettier-ignore
}
@computed get inkSubMenu() {
return (
- // prettier-ignore
<>
<PropertiesSection title="Appearance" isOpen={this.openAppearance} setIsOpen={bool => { this.openAppearance = bool; }} onDoubleClick={this.CloseAll}>
- {this.selectedLayoutDoc?.layout_isSvg ? this.appearanceEditor : null}
+ {this.selectedStrokes.length ? this.inkEditor : null}
</PropertiesSection>
<PropertiesSection title="Transform" isOpen={this.openTransform} setIsOpen={bool => { this.openTransform = bool; }} onDoubleClick={this.CloseAll}>
{this.transformEditor}
</PropertiesSection>
</>
- );
+ ); // prettier-ignore
}
+ /**
+ * Determines if a selected collection/group document contains any ink strokes to allow users to edit groups
+ * of ink strokes in the properties menu.
+ */
+ containsInk = (selectedDoc: Doc) => {
+ const childDocs: Doc[] = DocListCast(selectedDoc[DocData].data);
+ for (let i = 0; i < childDocs.length; i++) {
+ if (DocumentView.getDocumentView(childDocs[i])?.layoutDoc?.layout_isSvg) {
+ return true;
+ }
+ }
+ return false;
+ };
+
@computed get fieldsSubMenu() {
return (
<PropertiesSection
diff --git a/src/client/views/TagsView.tsx b/src/client/views/TagsView.tsx
index f44fd1d03..9858e7b61 100644
--- a/src/client/views/TagsView.tsx
+++ b/src/client/views/TagsView.tsx
@@ -274,12 +274,18 @@ export class TagsView extends ObservableReactComponent<TagViewProps> {
@observable _currentInput = '';
@observable _isEditing = !StrListCast(this.View.dataDoc.tags).length;
_heightDisposer: IReactionDisposer | undefined;
+ _lastXf = this.View.screenToContentsTransform();
componentDidMount() {
this._heightDisposer = reaction(
() => this.View.screenToContentsTransform(),
- () => {
- this._panelHeightDirty = this._panelHeightDirty + 1;
+ xf => {
+ if (xf.Scale === 0) return;
+ if (this.View.ComponentView?.isUnstyledView?.() || (!this.View.showTags && this._props.Views.length === 1)) return;
+ if (xf.TranslateX !== this._lastXf.TranslateX || xf.TranslateY !== this._lastXf.TranslateY || xf.Scale !== this._lastXf.Scale) {
+ this._panelHeightDirty = this._panelHeightDirty + 1;
+ }
+ this._lastXf = xf;
}
);
}
diff --git a/src/client/views/ViewBoxInterface.ts b/src/client/views/ViewBoxInterface.ts
index dce64ab92..f66f6062e 100644
--- a/src/client/views/ViewBoxInterface.ts
+++ b/src/client/views/ViewBoxInterface.ts
@@ -22,7 +22,7 @@ export abstract class ViewBoxInterface<P> extends ObservableReactComponent<React
return ''; //
}
promoteCollection?: () => void; // moves contents of collection to parent
- updateIcon?: () => void; // updates the icon representation of the document
+ updateIcon?: (usePanelDimensions?: boolean) => Promise<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/CollectionCardDeckView.tsx b/src/client/views/collections/CollectionCardDeckView.tsx
index 66014a1e0..37612ff5f 100644
--- a/src/client/views/collections/CollectionCardDeckView.tsx
+++ b/src/client/views/collections/CollectionCardDeckView.tsx
@@ -1,4 +1,4 @@
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, trace } from 'mobx';
+import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ClientUtils, DashColor, returnFalse, returnZero } from '../../../ClientUtils';
@@ -139,7 +139,7 @@ export class CollectionCardView extends CollectionSubView() {
* custom group
*/
@computed get childDocsWithoutLinks() {
- return this.childDocs.filter(l => l.type !== DocumentType.LINK);
+ return this.childDocs.filter(l => !l.layout_isSvg);
}
/**
@@ -402,7 +402,7 @@ export class CollectionCardView extends CollectionSubView() {
PanelHeight={this.childPanelHeight}
dontCenter="y" // Don't center it vertically, because the grid it's in is already doing that and we don't want to do it twice.
dragAction={(this.Document.childDragAction ?? this._props.childDragAction) as dropActionType}
- showTags={true}
+ showTags={BoolCast(this.layoutDoc.showChildTags)}
dontHideOnDrag
/>
);
@@ -607,6 +607,8 @@ export class CollectionCardView extends CollectionSubView() {
const dref = this._docRefs.get(doc);
const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(dref?.ContentDiv);
+ if (!scale) return new Transform(0, 0, 0);
+
return new Transform(-translateX + (dref?.centeringX || 0) * scale,
-translateY + (dref?.centeringY || 0) * scale, 1)
.scale(1 / scale).rotate(!isSelected ? -this.rotate(amCards, calcRowIndex) : 0); // prettier-ignore
@@ -639,7 +641,7 @@ export class CollectionCardView extends CollectionSubView() {
SnappingManager.SetIsResizing(undefined);
this._forceChildXf++;
}),
- 600
+ 1000
);
}
})}
diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx
index 139aebb02..c5da8e037 100644
--- a/src/client/views/collections/CollectionCarousel3DView.tsx
+++ b/src/client/views/collections/CollectionCarousel3DView.tsx
@@ -16,7 +16,7 @@ import './CollectionCarousel3DView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
import { Transform } from '../../util/Transform';
-// eslint-disable-next-line @typescript-eslint/no-var-requires
+// eslint-disable-next-line @typescript-eslint/no-require-imports
const { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } = require('../global/globalCssVariables.module.scss');
@observer
@@ -89,14 +89,13 @@ export class CollectionCarousel3DView extends CollectionSubView() {
const currentIndex = NumCast(this.layoutDoc._carousel_index);
const displayDoc = (childPair: { layout: Doc; data: Doc }, dxf: () => Transform) => (
<DocumentView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
Document={childPair.layout}
TemplateDataDocument={childPair.data}
// suppressSetHeight={true}
NativeWidth={returnZero}
NativeHeight={returnZero}
- fitWidth={undefined}
+ fitWidth={this._props.childLayoutFitWidth}
containerViewPath={this.childContainerViewPath}
onDoubleClickScript={this.onChildDoubleClick}
renderDepth={this._props.renderDepth + 1}
diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx
index 108cdbdb4..936226baf 100644
--- a/src/client/views/collections/CollectionCarouselView.tsx
+++ b/src/client/views/collections/CollectionCarouselView.tsx
@@ -1,6 +1,6 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Tooltip } from '@mui/material';
-import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
+import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { StopEvent, returnOne, returnZero } from '../../../ClientUtils';
@@ -10,13 +10,12 @@ import { DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { StyleProp } from '../StyleProp';
+import { TagItem } from '../TagsView';
import { DocumentView } from '../nodes/DocumentView';
import { FieldViewProps } from '../nodes/FieldView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import './CollectionCarouselView.scss';
import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView';
-import { TagItem } from '../TagsView';
-import { tickStep } from 'd3';
enum cardMode {
STAR = 'star',
@@ -179,9 +178,9 @@ export class CollectionCarouselView extends CollectionSubView() {
Document={doc}
NativeWidth={returnZero}
NativeHeight={returnZero}
- fitWidth={undefined}
- showTags={true}
+ fitWidth={this._props.childLayoutFitWidth}
hideFilterStatus={true}
+ showTags={BoolCast(this.layoutDoc.showChildTags)}
containerViewPath={this.childContainerViewPath}
setContentViewBox={undefined}
onDoubleClickScript={this.onContentDoubleClick}
@@ -199,6 +198,7 @@ export class CollectionCarouselView extends CollectionSubView() {
ScreenToLocalTransform={this.contentScreentToLocalXf}
PanelWidth={this.contentPanelWidth}
PanelHeight={this.contentPanelHeight}
+ xPadding={35}
/>
);
};
@@ -344,12 +344,12 @@ export class CollectionCarouselView extends CollectionSubView() {
)}
<div className="carouselView-practiceModes" style={{ display: BoolCast(curDoc?._layout_isFlashcard) ? undefined : 'none' }}>
<Tooltip title="Practice flashcards using GPT">
- <div key="back" className="carouselView-quiz" onClick={e => this.togglePracticeMode(practiceMode.QUIZ)}>
+ <div key="back" className="carouselView-quiz" onClick={() => this.togglePracticeMode(practiceMode.QUIZ)}>
<FontAwesomeIcon icon="file-pen" color={this.setColor(practiceMode.QUIZ, StrCast(this.practiceMode))} size="1x" />
</div>
</Tooltip>
<Tooltip title={this.practiceMode === practiceMode.PRACTICE ? 'Exit practice mode' : 'Practice flashcards manually'}>
- <div key="back" className="carouselView-practice" onClick={e => this.togglePracticeMode(practiceMode.PRACTICE)}>
+ <div key="back" className="carouselView-practice" onClick={() => this.togglePracticeMode(practiceMode.PRACTICE)}>
<FontAwesomeIcon icon="check" color={this.setColor(practiceMode.PRACTICE, StrCast(this.practiceMode))} size="1x" />
</div>
</Tooltip>
@@ -377,7 +377,7 @@ export class CollectionCarouselView extends CollectionSubView() {
) : (
<p
className="message"
- onClick={e => {
+ onClick={() => {
if (this.filterMessage || this.practiceMessage) {
this.setPracticeMode(undefined);
Doc.setDocFilter(this.layoutDoc, 'tags', Doc.FilterAny, 'remove');
diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx
index 028133a6e..d1304b8f4 100644
--- a/src/client/views/collections/CollectionDockingView.tsx
+++ b/src/client/views/collections/CollectionDockingView.tsx
@@ -203,7 +203,6 @@ export class CollectionDockingView extends CollectionSubView() {
} else if (instance._goldenLayout.root.contentItems[0].isRow) {
// if row
switch (pullSide) {
- // eslint-disable-next-line default-case-last
default:
case OpenWhereMod.none:
case OpenWhereMod.right:
@@ -299,7 +298,7 @@ export class CollectionDockingView extends CollectionSubView() {
this._goldenLayout.unbind('tabCreated', this.tabCreated);
this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
this._goldenLayout.unbind('stackCreated', this.stackCreated);
- } catch (e) {
+ } catch {
/* empty */
}
this.tabMap.clear();
@@ -380,7 +379,7 @@ export class CollectionDockingView extends CollectionSubView() {
try {
this._goldenLayout.unbind('stackCreated', this.stackCreated);
this._goldenLayout.unbind('tabDestroyed', this.tabDestroyed);
- } catch (e) {
+ } catch {
/* empty */
}
this._goldenLayout?.destroy();
@@ -507,8 +506,8 @@ export class CollectionDockingView extends CollectionSubView() {
dashboardDoc.myOverlayDocs = new List<Doc>();
dashboardDoc.myPublishedDocs = new List<Doc>();
- DashboardView.SetupDashboardTrails(dashboardDoc);
- DashboardView.SetupDashboardCalendars(dashboardDoc); // Zaul TODO: needed?
+ DashboardView.SetupDashboardTrails();
+ DashboardView.SetupDashboardCalendars(); // Zaul TODO: needed?
return DashboardView.openDashboard(dashboardDoc);
}
diff --git a/src/client/views/collections/CollectionMenu.scss b/src/client/views/collections/CollectionMenu.scss
index 3ec875df4..45d9394ed 100644
--- a/src/client/views/collections/CollectionMenu.scss
+++ b/src/client/views/collections/CollectionMenu.scss
@@ -6,7 +6,7 @@
align-content: center;
justify-content: space-between;
background-color: $dark-gray;
- height: 35px;
+ height: 40px;
border-bottom: $standard-border;
padding: 0 10px;
align-items: center;
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index d2514dfd1..5444a7a57 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -467,7 +467,7 @@ export class TreeView extends ObservableReactComponent<TreeViewProps> {
return false;
}
- refTransform = (ref: HTMLElement | undefined | null) => {
+ refTransform = (ref: HTMLDivElement | undefined | null) => {
if (!ref) return this.ScreenToLocalTransform();
const { translateX, translateY, scale } = ClientUtils.GetScreenTransform(ref);
return new Transform(-translateX, -translateY, 1).scale(1 / scale);
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index f106eba26..0cc63d632 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -30,7 +30,7 @@ import { CompileScript } from '../../../util/Scripting';
import { ScriptingGlobals } from '../../../util/ScriptingGlobals';
import { freeformScrollMode, SnappingManager } from '../../../util/SnappingManager';
import { Transform } from '../../../util/Transform';
-import { undoable, UndoManager } from '../../../util/UndoManager';
+import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager';
import { Timeline } from '../../animationtimeline/Timeline';
import { ContextMenu } from '../../ContextMenu';
import { InkingStroke } from '../../InkingStroke';
@@ -42,6 +42,8 @@ import { FocusViewOptions } from '../../nodes/FocusViewOptions';
import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox';
import { OpenWhere, OpenWhereMod } from '../../nodes/OpenWhere';
import { PinDocView, PinProps } from '../../PinFuncs';
+import { AnnotationPalette } from '../../smartdraw/AnnotationPalette';
+import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
import { StyleProp } from '../../StyleProp';
import { CollectionSubView, SubCollectionViewProps } from '../CollectionSubView';
import { TreeViewType } from '../CollectionTreeViewType';
@@ -115,6 +117,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@observable _panZoomTransition: number = 0; // sets the pan/zoom transform ease time- used by nudge(), focus() etc to smoothly zoom/pan. set to 0 to use document's transition time or default of 0
@observable _firstRender = false; // this turns off rendering of the collection's content so that there's instant feedback when a tab is switched of what content will be shown. could be used for performance improvement
@observable _showAnimTimeline = false;
+ @observable _showDrawingEditor = false;
@observable _deleteList: DocumentView[] = [];
@observable _timelineRef = React.createRef<Timeline>();
@observable _marqueeViewRef = React.createRef<MarqueeView>();
@@ -492,28 +495,31 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (!this.Document.isGroup) {
// group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag
// prettier-ignore
+ const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
switch (Doc.ActiveTool) {
- case InkTool.Highlighter: break;
- case InkTool.Write: break;
- case InkTool.Pen: break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
+ case InkTool.Highlighter:
+ case InkTool.Write:
+ case InkTool.Pen:
+ break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
case InkTool.StrokeEraser:
case InkTool.SegmentEraser:
- this._batch = UndoManager.StartBatch('collectionErase');
- this._eraserPts.length = 0;
- setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction);
- break;
case InkTool.RadiusEraser:
this._batch = UndoManager.StartBatch('collectionErase');
this._eraserPts.length = 0;
- setupMoveUpEvents(this, e, this.onRadiusEraserMove, this.onEraserUp, emptyFunction);
+ setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, this.onEraserClick, hit !== -1);
+ e.stopPropagation();
+ break;
+ case InkTool.SmartDraw:
+ setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, () => this.showSmartDraw(e.pageX, e.pageY), hit !== -1);
+ e.stopPropagation();
break;
case InkTool.None:
if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) {
- const hit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
- setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, hit !== -1, false);
+ const ahit = this._clusters.handlePointerDown(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY));
+ setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, ahit !== -1, false);
}
break;
- default:
+ default:
}
}
}
@@ -536,24 +542,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
default: {
const { points } = ge;
const B = this.screenToFreeformContentsXf.transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height);
- const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
- const inkDoc = Docs.Create.InkDocument(
- points,
- { title: ge.gesture.toString(),
- x: B.x - inkWidth / 2,
- y: B.y - inkWidth / 2,
- _width: B.width + inkWidth,
- _height: B.height + inkWidth,
- stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
- inkWidth,
- ActiveInkColor(),
- ActiveInkBezierApprox(),
- ActiveFillColor(),
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveDash(),
- ActiveIsInkMask()
- );
+ const inkDoc = this.createInkDoc(points, B);
if (Doc.ActiveTool === InkTool.Write) {
this.unprocessedDocs.push(inkDoc);
CollectionFreeFormView.collectionsWithUnprocessedInk.add(this);
@@ -605,96 +594,76 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
/**
* Erases strokes by intersecting them with an invisible "eraser stroke".
- * By default this iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments,
- * and deletes the original stroke.
+ * By default this iterates through all intersected ink strokes, determines which parts of a stroke need to be erased based on the type
+ * of eraser, draws back the ink segments to keep, and deletes the original stroke.
+ *
+ * Radius eraser: erases strokes by intersecting them with a circle of variable radius. Essentially creates an InkField for the
+ * eraser circle, then determines its intersections with other ink strokes. Each stroke's DocumentView and its
+ * intersection t-values are put into a map, which gets looped through to take out the erased parts.
*/
- @action
- onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
+ erase = (e: PointerEvent, delta: number[]) => {
+ e.stopImmediatePropagation();
const currPoint = { X: e.clientX, Y: e.clientY };
this._eraserPts.push([currPoint.X, currPoint.Y]);
this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5));
- // if (this._eraserLock) return false; // leaving this commented out in case the idea is revisited in the future
- this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => {
- if (!this._deleteList.includes(intersect.inkView)) {
- this._deleteList.push(intersect.inkView);
- SetActiveInkWidth(StrCast(intersect.inkView.Document.stroke_width?.toString()) || '1');
- SetActiveInkColor(StrCast(intersect.inkView.Document.color?.toString()) || 'black');
- // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
- if (Doc.ActiveTool !== InkTool.StrokeEraser) {
- // this._eraserLock++;
- const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it
- const newStrokes = segments?.map(segment => {
- const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]);
- const bounds = InkField.getBounds(points);
- const B = this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
- const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
- return Docs.Create.InkDocument(
- points,
- { title: 'stroke',
- x: B.x - inkWidth / 2,
- y: B.y - inkWidth / 2,
- _width: B.width + inkWidth,
- _height: B.height + inkWidth,
- stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
- inkWidth,
- ActiveInkColor(),
- ActiveInkBezierApprox(),
- ActiveFillColor(),
- ActiveArrowStart(),
- ActiveArrowEnd(),
- ActiveDash(),
- ActiveIsInkMask()
- );
- });
- newStrokes && this.addDocument?.(newStrokes);
- // setTimeout(() => this._eraserLock--);
+ if (Doc.ActiveTool === InkTool.RadiusEraser) {
+ const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
+ strokeMap.forEach((intersects, stroke) => {
+ if (!this._deleteList.includes(stroke)) {
+ this._deleteList.push(stroke);
+ SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1');
+ SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black');
+ const segments = this.radiusErase(stroke, intersects.sort());
+ segments?.forEach(segment =>
+ this.forceStrokeGesture(
+ e,
+ Gestures.Stroke,
+ segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
+ )
+ );
}
- // Lower ink opacity to give the user a visual indicator of deletion.
- intersect.inkView.layoutDoc.opacity = 0;
- intersect.inkView.layoutDoc.dontIntersect = true;
- }
- });
+ stroke.layoutDoc.opacity = 0;
+ stroke.layoutDoc.dontIntersect = true;
+ });
+ } else {
+ this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => {
+ if (!this._deleteList.includes(intersect.inkView)) {
+ this._deleteList.push(intersect.inkView);
+ SetActiveInkWidth(StrCast(intersect.inkView.Document.stroke_width?.toString()) || '1');
+ SetActiveInkColor(StrCast(intersect.inkView.Document.color?.toString()) || 'black');
+ // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
+ if (Doc.ActiveTool !== InkTool.StrokeEraser) {
+ // this._eraserLock++;
+ const segments = this.segmentErase(intersect.inkView, intersect.t); // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it
+ const newStrokes = segments?.map(segment => {
+ const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]);
+ return this.createInkDoc(points);
+ });
+ newStrokes && this.addDocument?.(newStrokes);
+ // setTimeout(() => this._eraserLock--);
+ }
+ }
+ });
+ }
return false;
};
- /**
- * Erases strokes by intersecting them with a circle of variable radius. Essentially creates an InkField for the
- * eraser circle, then determines its intersections with other ink strokes. Each stroke's DocumentView and its
- * intersection t-values are put into a map, which gets looped through to take out the erased parts.
- * @param e
- * @param down
- * @param delta
- * @returns
- */
@action
- onRadiusEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
- const currPoint = { X: e.clientX, Y: e.clientY };
- this._eraserPts.push([currPoint.X, currPoint.Y]);
- this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5));
- const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint);
-
- strokeMap.forEach((intersects, stroke) => {
- if (!this._deleteList.includes(stroke)) {
- this._deleteList.push(stroke);
- SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1');
- SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black');
- const segments = this.radiusErase(stroke, intersects.sort());
- segments?.forEach(segment =>
- this.forceStrokeGesture(
- e,
- Gestures.Stroke,
- segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[])
- )
- );
- }
- stroke.layoutDoc.opacity = 0;
- stroke.layoutDoc.dontIntersect = true;
- });
+ onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => {
+ this.erase(e, delta);
+ // if (this._eraserLock) return false; // leaving this commented out in case the idea is revisited in the future
return false;
};
- forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData) => {
- this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points)));
+ @action
+ onEraserClick = (e: PointerEvent) => {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ this.erase(e, [0, 0]);
+ };
+
+ forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: string) => {
+ this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points), text));
};
onPointerMove = (e: PointerEvent) => {
@@ -722,7 +691,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
// increase radius slightly based on the erased stroke's width, added to make eraser look more realistic
const radius = ActiveEraserWidth() + 5 + inkStrokeWidth * 0.1; // add 5 to prevent eraser from being too small
const c = 0.551915024494; // circle tangent length to side ratio
- const movement = { x: endInkCoordsIn.X - startInkCoordsIn.X, y: endInkCoordsIn.Y - startInkCoordsIn.Y };
+ const movement = { x: Math.max(endInkCoordsIn.X - startInkCoordsIn.X, 1), y: Math.max(endInkCoordsIn.Y - startInkCoordsIn.Y, 1) };
const moveLen = Math.sqrt(movement.x ** 2 + movement.y ** 2);
const direction = { x: (movement.x / moveLen) * radius, y: (movement.y / moveLen) * radius };
const normal = { x: -direction.y, y: direction.x }; // prettier-ignore
@@ -889,13 +858,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
strokeToTVals.set(inkView, [Math.floor(inkData.length / 4) + 1]);
}
}
-
for (let i = 0; i < inkData.length - 3; i += 4) {
// iterate over each segment of bezier curve
for (let j = 0; j < eraserInkData.length - 3; j += 4) {
const intersectCurve: Bezier = InkField.Segment(inkData, i); // other curve
const eraserCurve: Bezier = InkField.Segment(eraserInkData, j); // eraser curve
- this.bintersects(intersectCurve, eraserCurve).forEach((val: string | number, k: number) => {
+ InkField.bintersects(intersectCurve, eraserCurve).forEach((val: string | number, k: number) => {
// Converting the Bezier.js Split type to a t-value number.
const t = +val.toString().split('/')[0];
if (k % 2 === 0) {
@@ -1169,26 +1137,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return this.getClosestTs(tVals, excludeT, startIndex, mid - 1);
};
- // for some reason bezier.js doesn't handle the case of intersecting a linear curve, so we wrap the intersection
- // call in a test for linearity
- bintersects = (curve: Bezier, otherCurve: Bezier) => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- if ((curve as any)._linear) {
- // bezier.js doesn't intersect properly if the curve is actually a line -- so get intersect other curve against this line, then figure out the t coordinates of the intersection on this line
- const intersections = otherCurve.lineIntersects({ p1: curve.points[0], p2: curve.points[3] });
- if (intersections.length) {
- const intPt = otherCurve.get(intersections[0]);
- const intT = curve.project(intPt).t;
- return intT ? [intT] : [];
- }
- }
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- if ((otherCurve as any)._linear) {
- return curve.lineIntersects({ p1: otherCurve.points[0], p2: otherCurve.points[3] });
- }
- return curve.intersects(otherCurve);
- };
-
/**
* Determines all possible intersections of the current curve of the intersected ink stroke with all other curves of all
* ink strokes in the current collection.
@@ -1220,7 +1168,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
if (apt.d !== undefined && apt.d < 1 && apt.t !== undefined && !tVals.includes(apt.t)) {
tVals.push(apt.t);
}
- this.bintersects(curve, otherCurve).forEach((val: string | number, ival: number) => {
+ InkField.bintersects(curve, otherCurve).forEach((val: string | number, ival: number) => {
// Converting the Bezier.js Split type to a t-value number.
const t = +val.toString().split('/')[0];
if (ival % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack! don't know why but intersection points are doubled from bezier.js (but not identical).
@@ -1233,6 +1181,108 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
return tVals;
};
+ /**
+ * Creates an ink document to add to the freeform canvas.
+ */
+ createInkDoc = (points: InkData, transformedBounds?: { x: number; y: number; width: number; height: number }) => {
+ const bounds = InkField.getBounds(points);
+ const B = transformedBounds || this.screenToFreeformContentsXf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+ const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
+ return Docs.Create.InkDocument(
+ points,
+ { title: 'stroke',
+ x: B.x - inkWidth / 2,
+ y: B.y - inkWidth / 2,
+ _width: B.width + inkWidth,
+ _height: B.height + inkWidth,
+ stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+ inkWidth,
+ ActiveInkColor(),
+ ActiveInkBezierApprox(),
+ ActiveFillColor(),
+ ActiveArrowStart(),
+ ActiveArrowEnd(),
+ ActiveDash(),
+ ActiveIsInkMask()
+ );
+ };
+
+ @action
+ showSmartDraw = (x: number, y: number) => {
+ SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+ SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+ SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+ SmartDrawHandler.Instance.displaySmartDrawHandler(x, y);
+ };
+
+ _drawing: Doc[] = [];
+ _drawingContainer: Doc | undefined = undefined;
+ /**
+ * Function that creates a drawing--a group of ink strokes--to go with the smart draw function.
+ */
+ @undoBatch
+ createDrawingDoc = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
+ this._drawing = [];
+ const xf = this.screenToFreeformContentsXf;
+ 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);
+ });
+ return MarqueeView.getCollection(this._drawing, undefined, true, { left: opts.x, top: opts.y, width: 1, height: 1 });
+ };
+
+ /**
+ * Part of regenerating a drawing--deletes the old drawing.
+ */
+ removeDrawing = (useLastContainer: boolean, doc?: Doc) => {
+ this._batch = UndoManager.StartBatch('regenerateDrawing');
+ if (useLastContainer && this._drawingContainer) {
+ this._props.removeDocument?.(this._drawingContainer);
+ } else if (doc) {
+ const docData = doc[DocData];
+ const children = DocListCast(docData.data);
+ this._props.removeDocument?.(doc);
+ this._props.removeDocument?.(children);
+ }
+ this._drawing = [];
+ };
+
+ /**
+ * Adds the created drawing to the freeform canvas and sets the metadata.
+ */
+ addDrawing = (doc: Doc, opts: DrawingOptions, gptRes: string) => {
+ const docData = doc[DocData];
+ docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
+ docData.width = opts.size;
+ docData.drawingInput = opts.text;
+ docData.drawingComplexity = opts.complexity;
+ docData.drawingColored = opts.autoColor;
+ docData.drawingSize = opts.size;
+ docData.drawingData = gptRes;
+ this._drawingContainer = doc;
+ this.addDocument(doc);
+ this._batch?.end();
+ };
+
@action
zoom = (pointX: number, pointY: number, deltaY: number): void => {
if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
@@ -1803,33 +1853,34 @@ 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;
- }
- );
+ return !contentDiv
+ ? new Promise<void>(res => res())
+ : 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) => {
- this._eraserX = e.clientX;
- this._eraserY = e.clientY;
- // super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
+ const locPt = this.ScreenToLocalBoxXf().transformPoint(e.clientX, e.clientY);
+ this._eraserX = locPt[0];
+ this._eraserY = locPt[1];
};
@action
@@ -1910,7 +1961,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
icon: 'eye',
});
appearanceItems.push({ description: `Pin View`, event: () => this._props.pinToPres(this.Document, { pinViewport: MarqueeView.CurViewBounds(this.dataDoc, this._props.PanelWidth(), this._props.PanelHeight()) }), icon: 'map-pin' });
- !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: 'compress-arrows-alt' });
+ !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: () => this.updateIcon(), icon: 'compress-arrows-alt' });
this._props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' });
this.Document.isGroup && this.Document.transcription && appearanceItems.push({ description: 'Ink to text', event: this.transcribeStrokes, icon: 'font' });
@@ -1930,6 +1981,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}),
icon: 'eye',
});
+ optionItems.push({
+ description: 'Show Drawing Editor',
+ event: action(() => {
+ SmartDrawHandler.Instance.CreateDrawingDoc = this.createDrawingDoc;
+ SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+ SmartDrawHandler.Instance.RemoveDrawing = this.removeDrawing;
+ !SmartDrawHandler.Instance.ShowRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10) : SmartDrawHandler.Instance.hideRegenerate();
+ }),
+ icon: 'pen-to-square',
+ });
+ optionItems.push({
+ description: this.Document.savedAsAnno ? 'Saved as Annotation!' : 'Save to Annotation Palette',
+ event: action(undoable(async () => await AnnotationPalette.addToPalette(this.Document), 'save to palette')),
+ icon: this.Document.savedAsAnno ? 'clipboard-check' : 'file-arrow-down',
+ });
this._props.renderDepth &&
optionItems.push({
description: 'Use Background Color as Default',
@@ -1945,6 +2011,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
!options && ContextMenu.Instance.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' });
const mores = ContextMenu.Instance.findByDescription('More...');
const moreItems = mores?.subitems ?? [];
+ moreItems.push({
+ description: 'recognize all ink',
+ event: () => {
+ this.unprocessedDocs.push(...this.childDocs.filter(doc => doc.type === DocumentType.INK));
+ CollectionFreeFormView.collectionsWithUnprocessedInk.add(this);
+ },
+ icon: 'pen',
+ });
!mores && ContextMenu.Instance.addItem({ description: 'More...', subitems: moreItems, icon: 'eye' });
};
@@ -2133,8 +2207,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
onPointerMove={this.onCursorMove}
style={{
position: 'fixed',
- left: this._eraserX - 60,
- top: this._eraserY - 100,
+ left: this._eraserX,
+ top: this._eraserY,
width: (ActiveEraserWidth() + 5) * 2,
height: (ActiveEraserWidth() + 5) * 2,
borderRadius: '50%',
diff --git a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss
index e7413bf8e..9b8727e1a 100644
--- a/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss
+++ b/src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss
@@ -42,3 +42,17 @@
}
}
}
+
+.complexity-slider {
+ width: 50%; /* Full-width */
+ height: 25px; /* Specified height */
+ background: #d3d3d3; /* Grey background */
+ outline: none; /* Remove outline */
+ opacity: 0.7; /* Set transparency (for mouse-over effects on hover) */
+ -webkit-transition: 0.2s; /* 0.2 seconds transition on hover */
+ transition: opacity 0.2s;
+
+ :hover {
+ opacity: 1; /* Fully shown on mouse-over */
+ }
+}
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
index 44c916ab9..de65b240f 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 917aaaea8..c865c681d 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -2,11 +2,11 @@ import { action, computed, makeObservable, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ClientUtils, lightOrDark, returnFalse } from '../../../../ClientUtils';
-import { intersectRect } from '../../../../Utils';
+import { intersectRect, unimplementedFunction } from '../../../../Utils';
import { Doc, DocListCast, Opt } from '../../../../fields/Doc';
import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols';
import { Id } from '../../../../fields/FieldSymbols';
-import { InkTool } from '../../../../fields/InkField';
+import { InkData, InkTool } from '../../../../fields/InkField';
import { List } from '../../../../fields/List';
import { Cast, NumCast, StrCast } from '../../../../fields/Types';
import { ImageField } from '../../../../fields/URLField';
@@ -57,6 +57,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
return { left: NumCast(pinDoc._freeform_panX) - panelWidth / 2 / ps, top: NumCast(pinDoc._freeform_panY) - panelHeight / 2 / ps, width: panelWidth / ps, height: panelHeight / ps };
}
+ // eslint-disable-next-line no-use-before-define
static Instance: MarqueeView;
constructor(props: SubCollectionViewProps & MarqueeViewProps) {
@@ -88,6 +89,8 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
return bounds;
}
+ public AddInkDoc: (points: InkData) => Doc | void = unimplementedFunction;
+
componentDidMount() {
this._props.setPreviewCursor?.(this.setPreviewCursor);
}
@@ -363,7 +366,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) => {
@@ -375,14 +378,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;
});
@@ -419,7 +421,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;
@@ -427,6 +429,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
this._props.selectDocuments([newCollection]);
MarqueeOptionsMenu.Instance.fadeOut(true);
this.hideMarquee();
+ return newCollection;
});
/**
@@ -455,20 +458,19 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
let x_offset = 0;
let y_offset = 0;
let row_count = 0;
+ const newColDim = 900;
for (const label of labelGroups) {
- const newCollection = this.getCollection([], undefined, false);
- newCollection._width = 900;
- newCollection._height = 900;
- newCollection._x = this.Bounds.left;
- newCollection._y = this.Bounds.top;
+ const newCollection = MarqueeView.getCollection([], undefined, false, this.Bounds);
+ newCollection._x = this.Bounds.left + x_offset;
+ newCollection._y = this.Bounds.top + y_offset;
+ newCollection._width = newColDim;
+ newCollection._height = newColDim;
newCollection._freeform_panX = this.Bounds.left + this.Bounds.width / 2;
newCollection._freeform_panY = this.Bounds.top + this.Bounds.height / 2;
- newCollection._x = (newCollection._x as number) + x_offset;
- newCollection._y = (newCollection._y as number) + y_offset;
- x_offset += (newCollection._width as number) + 40;
+ x_offset += newColDim + 40;
row_count += 1;
if (row_count == 3) {
- y_offset += (newCollection._height as number) + 40;
+ y_offset += newColDim + 40;
x_offset = 0;
row_count = 0;
}
@@ -547,7 +549,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
};
touchesLine(r1: { left: number; top: number; width: number; height: number }) {
- // eslint-disable-next-line no-restricted-syntax
for (const lassoPt of this._lassoPts) {
const topLeft = this.Transform.transformPoint(lassoPt[0], lassoPt[1]);
if (r1.left < topLeft[0] && topLeft[0] < r1.left + r1.width && r1.top < topLeft[1] && topLeft[1] < r1.top + r1.height) {
@@ -568,7 +569,6 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
let hasLeft = false;
let hasBottom = false;
let hasRight = false;
- // eslint-disable-next-line no-restricted-syntax
for (const lassoPt of this._lassoPts) {
const truePoint = this.Transform.transformPoint(lassoPt[0], lassoPt[1]);
hasLeft = hasLeft || (truePoint[0] > tl[0] && truePoint[0] < r1.left && truePoint[1] > r1.top && truePoint[1] < r1.top + r1.height);
@@ -685,8 +685,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
<div
className="marqueeView"
ref={r => {
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- r?.addEventListener('dashDragMovePause', this.onDragMovePause as any);
+ r?.addEventListener('dashDragMovePause', this.onDragMovePause as EventListenerOrEventListenerObject);
this.MarqueeRef = r;
}}
style={{
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
index 8b0639b3b..325628d53 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx
@@ -32,7 +32,6 @@ import './CollectionSchemaView.scss';
import { SchemaColumnHeader } from './SchemaColumnHeader';
import { SchemaRowBox } from './SchemaRowBox';
-// eslint-disable-next-line @typescript-eslint/no-var-requires
const { SCHEMA_NEW_NODE_HEIGHT } = require('../../global/globalCssVariables.module.scss'); // prettier-ignore
export const FInfotoColType: { [key: string]: ColumnType } = {
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index 47cc07ce1..423a2d6ef 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -1,7 +1,6 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { Colors } from 'browndash-components';
-import { action, runInAction } from 'mobx';
-import { aggregateBounds } from '../../../Utils';
+import { runInAction } from 'mobx';
import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { InkTool } from '../../../fields/InkField';
@@ -15,6 +14,7 @@ import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SnappingManager } from '../../util/SnappingManager';
import { UndoManager, undoable } from '../../util/UndoManager';
import { GestureOverlay } from '../GestureOverlay';
+import { InkTranscription } from '../InkTranscription';
import { InkingStroke } from '../InkingStroke';
import { MainView } from '../MainView';
import { PropertiesView } from '../PropertiesView';
@@ -135,7 +135,11 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) {
});
// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function showFreeform(attr: 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce', checkResult?: boolean, persist?: boolean) {
+ScriptingGlobals.add(function showFreeform(
+ attr: 'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down' | 'pile' | 'toggle-chat' | 'toggle-tags' | 'tag',
+ checkResult?: boolean,
+ persist?: boolean
+) {
const selected = DocumentView.SelectedDocs().lastElement();
function isAttrFiltered(attribute: string) {
@@ -143,7 +147,7 @@ ScriptingGlobals.add(function showFreeform(attr: 'hcenter' | 'vcenter' | 'grid'
}
// prettier-ignore
- const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down' | 'pile' | 'toggle-chat' | 'tag',
+ const map: Map<'flashcards' | 'hcenter' | 'vcenter' | 'grid' | 'snaplines' | 'clusters' | 'viewAll' | 'fitOnce' | 'time' | 'docType' | 'color' | 'chat' | 'up' | 'down' | 'pile' | 'toggle-chat' | 'toggle-tags' | 'tag',
{
waitForRender?: boolean;
checkResult: (doc: Doc) => boolean;
@@ -227,6 +231,12 @@ ScriptingGlobals.add(function showFreeform(attr: 'hcenter' | 'vcenter' | 'grid'
},
}],
+ ['toggle-tags', {
+ checkResult: (doc: Doc) => BoolCast(doc?.showChildTags),
+ setDoc: (doc: Doc, dv: DocumentView) => {
+ doc.showChildTags = !doc.showChildTags;
+ },
+ }],
['pile', {
checkResult: (doc: Doc) => doc._type_collection == CollectionViewType.Freeform,
setDoc: (doc: Doc, dv: DocumentView) => {
@@ -383,76 +393,11 @@ ScriptingGlobals.add(function toggleCharStyle(charStyle: attrname, checkResult?:
return undefined;
});
-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) {
- 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;
- // loop through selected an get the bound
- const bounds: { x: number; y: number; width?: number; height?: number }[] = [];
-
- 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 });
- })
- );
-
- const aggregBounds = aggregateBounds(bounds, 0, 0);
- const marqViewRef = ffView._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;
- }
-
- 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;
- })
- );
- ffView._props.removeDocument?.(selected);
- // TODO: nda - this is the code to actually get a new grouped collection
- const newCollection = marqViewRef?.getCollection(selected, undefined, true);
- if (newCollection) {
- newCollection.height = NumCast(newCollection._height);
- newCollection.width = NumCast(newCollection._width);
- }
-
- // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs
- newCollection && ffView._props.addDocument?.(newCollection);
- // TODO: nda - will probably need to go through and only remove the unprocessed selected docs
- ffView.unprocessedDocs = [];
-
- // InkTranscription.Instance.transcribeInk(newCollection, selected, false);
- });
- }
- CollectionFreeFormView.collectionsWithUnprocessedInk.clear();
-}
-
-function setActiveTool(tool: InkTool | Gestures, keepPrim: boolean, checkResult?: boolean) {
- // InkTranscription.Instance?.createInkGroup();
+function setActiveTool(toolIn: InkTool | Gestures, keepPrim: boolean, checkResult?: boolean) {
+ InkTranscription.Instance?.createInkGroup();
+ const tool = toolIn === InkTool.Eraser ? Doc.UserDoc().activeEraserTool : toolIn;
if (checkResult) {
- return (Doc.ActiveTool === tool && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool
+ return ((Doc.ActiveTool === tool || (Doc.UserDoc().activeEraserTool === tool && (tool === toolIn || Doc.ActiveTool === tool))) && !GestureOverlay.Instance?.InkShape) || GestureOverlay.Instance?.InkShape === tool
? GestureOverlay.Instance?.KeepPrimitiveMode || ![Gestures.Circle, Gestures.Line, Gestures.Rectangle].includes(tool as Gestures)
: false;
}
@@ -529,7 +474,7 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fil
setMode: () => { SetActiveInkColor(StrCast(value)); selected?.type === DocumentType.INK && setActiveTool(GestureOverlay.Instance.InkShape ?? InkTool.Pen, true, false);},
}],
[ 'eraserWidth', {
- checkResult: () => ActiveEraserWidth(),
+ checkResult: () => ActiveEraserWidth() === 0 ? 1 : ActiveEraserWidth(),
setInk: (doc: Doc) => { },
setMode: () => { SetEraserWidth(+value);},
}]
diff --git a/src/client/views/linking/.LinkRelationshipSearch.tsx.icloud b/src/client/views/linking/.LinkRelationshipSearch.tsx.icloud
deleted file mode 100644
index 6e73d1bd0..000000000
--- a/src/client/views/linking/.LinkRelationshipSearch.tsx.icloud
+++ /dev/null
Binary files differ
diff --git a/src/client/views/nodes/.FaceRectangles.tsx.icloud b/src/client/views/nodes/.FaceRectangles.tsx.icloud
deleted file mode 100644
index 14c960022..000000000
--- a/src/client/views/nodes/.FaceRectangles.tsx.icloud
+++ /dev/null
Binary files differ
diff --git a/src/client/views/nodes/.LinkAnchorBox.tsx.icloud b/src/client/views/nodes/.LinkAnchorBox.tsx.icloud
deleted file mode 100644
index 2b5650f75..000000000
--- a/src/client/views/nodes/.LinkAnchorBox.tsx.icloud
+++ /dev/null
Binary files differ
diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx
index 6a918664c..2c0a102f5 100644
--- a/src/client/views/nodes/AudioBox.tsx
+++ b/src/client/views/nodes/AudioBox.tsx
@@ -8,7 +8,7 @@ import { DateField } from '../../../fields/DateField';
import { Doc, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, DateCast, DocCast, NumCast } from '../../../fields/Types';
+import { Cast, DateCast, NumCast } from '../../../fields/Types';
import { AudioField, nullAudio } from '../../../fields/URLField';
import { formatTime } from '../../../Utils';
import { Docs } from '../../documents/Documents';
@@ -738,7 +738,6 @@ export class AudioBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
ref={action((r: CollectionStackedTimeline | null) => {
this._stackedTimeline = r;
})}
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
dataFieldKey={this.fieldKey}
fieldKey={this.annotationKey}
diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx
index 461a65c27..9fb8bc4d6 100644
--- a/src/client/views/nodes/ComparisonBox.tsx
+++ b/src/client/views/nodes/ComparisonBox.tsx
@@ -1,41 +1,34 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
-import { MathJax, MathJaxContext } from 'better-react-mathjax';
import { Tooltip } from '@mui/material';
-import { action, computed, makeObservable, observable, reaction } from 'mobx';
+import axios from 'axios';
+import { IReactionDisposer, action, computed, makeObservable, observable, reaction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
+import ReactLoading from 'react-loading';
import { returnFalse, returnNone, returnTrue, returnZero, setupMoveUpEvents } from '../../../ClientUtils';
import { emptyFunction } from '../../../Utils';
import { Doc, Opt } from '../../../fields/Doc';
import { DocData } from '../../../fields/DocSymbols';
+import { Id } from '../../../fields/FieldSymbols';
import { RichTextField } from '../../../fields/RichTextField';
import { DocCast, NumCast, RTFCast, StrCast, toList } from '../../../fields/Types';
+import { nullAudio } from '../../../fields/URLField';
import { GPTCallType, gptAPICall, gptImageLabel } from '../../apis/gpt/GPT';
-import '../pdf/GPTPopup/GPTPopup.scss';
-import { DocUtils } from '../../documents/DocUtils';
+import { DocUtils, FollowLinkScript } from '../../documents/DocUtils';
import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes';
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { dropActionType } from '../../util/DropActionTypes';
import { undoable } from '../../util/UndoManager';
+import { ContextMenu } from '../ContextMenu';
import { ViewBoxAnnotatableComponent } from '../DocComponent';
import { PinDocView, PinProps } from '../PinFuncs';
import { StyleProp } from '../StyleProp';
+import '../pdf/GPTPopup/GPTPopup.scss';
import './ComparisonBox.scss';
import { DocumentView } from './DocumentView';
import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
-import ReactLoading from 'react-loading';
-import { ContextMenu } from '../ContextMenu';
-import { ContextMenuProps } from '../ContextMenuItem';
-import { tickStep } from 'd3';
-import { CollectionCarouselView } from '../collections/CollectionCarouselView';
-import { FollowLinkScript } from '../../documents/DocUtils';
-import { schema } from '../nodes/formattedText/schema_rts';
-import { Id } from '../../../fields/FieldSymbols';
-import axios from 'axios';
-import ReactMarkdown from 'react-markdown';
-import { WebField, nullAudio } from '../../../fields/URLField';
const API_URL = 'https://api.unsplash.com/search/photos';
@@ -44,7 +37,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
public static LayoutString(fieldKey: string) {
return FieldView.LayoutString(ComparisonBox, fieldKey);
}
+ private SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
+ private _closeRef = React.createRef<HTMLDivElement>();
private _disposers: (DragManager.DragDropDisposer | undefined)[] = [undefined, undefined];
+ private _reactDisposer: IReactionDisposer | undefined;
constructor(props: FieldViewProps) {
super(props);
makeObservable(this);
@@ -55,38 +51,19 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
@observable private _outputValue = '';
@observable private _loading = false;
@observable private _isEmpty = false;
- @observable private _audio: Doc = Docs.Create.TextDocument('');
- @observable childActive = false;
- @observable _yRelativeToTop: boolean = true;
- @observable _animating = '';
- @observable mathJaxConfig = {
- loader: { load: ['input/asciimath'] },
- };
+ @observable private _childActive = false;
+ @observable private _animating = '';
@observable private _listening = false;
- @observable transcriptElement = '';
- @observable private frontSide = false;
- SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
+ @observable private _frontSide = false;
@observable recognition = new this.SpeechRecognition();
- _closeRef = React.createRef<HTMLDivElement>();
- get revealOp() {
- return this.layoutDoc[`_${this._props.fieldKey}_revealOp`];
- }
- get clipHeightKey() {
- return '_' + this._props.fieldKey + '_clipHeight';
- }
-
- get clipWidthKey() {
- return '_' + this._props.fieldKey + '_clipWidth';
- }
-
- @computed get clipWidth() {
- return NumCast(this.layoutDoc[this.clipWidthKey], 50);
- }
-
- @computed get clipHeight() {
- return NumCast(this.layoutDoc[this.clipHeightKey], 200);
- }
+ @computed get revealOpKey() { return `_${this._props.fieldKey}_revealOp`; } // prettier-ignore
+ @computed get clipHeightKey() { return `_${this._props.fieldKey}_clipHeight`; } // prettier-ignore
+ @computed get clipWidthKey() { return `_${this._props.fieldKey}_clipWidth`; } // prettier-ignore
+ @computed get clipWidth() { return NumCast(this.layoutDoc[this.clipWidthKey], 50); } // prettier-ignore
+ @computed get clipHeight() { return NumCast(this.layoutDoc[this.clipHeightKey], 200); } // prettier-ignore
+ @computed get revealOp() { return StrCast(this.layoutDoc[this.revealOpKey]); } // prettier-ignore
+ set revealOp(value:string) { this.layoutDoc[this.revealOpKey] = value; } // prettier-ignore
@computed get overlayAlternateIcon() {
return (
@@ -101,8 +78,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
})
}
style={{
- background: this.revealOp === 'hover' ? 'gray' : this.frontSide ? 'white' : 'black',
- color: this.revealOp === 'hover' ? 'black' : this.frontSide ? 'black' : 'white',
+ background: this.revealOp === 'hover' ? 'gray' : this._frontSide ? 'white' : 'black',
+ color: this.revealOp === 'hover' ? 'black' : this._frontSide ? 'black' : 'white',
display: 'inline-block',
}}>
<div key="alternate" className="formattedTextBox-flip">
@@ -117,27 +94,27 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
return (
<div>
<Tooltip
- title={this.frontSide ? <div className="dash-tooltip">Flip to front side to use GPT</div> : <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard based on your question on the front</div>}>
- <div style={{ position: 'absolute', bottom: '3px', right: '50px', cursor: 'pointer' }} onPointerDown={e => (!this.frontSide ? this.findImageTags() : null)}>
+ title={this._frontSide ? <div className="dash-tooltip">Flip to front side to use GPT</div> : <div className="dash-tooltip">Ask GPT to create an answer on the back side of the flashcard based on your question on the front</div>}>
+ <div style={{ position: 'absolute', bottom: '3px', right: '50px', cursor: 'pointer' }} onPointerDown={() => (!this._frontSide ? this.findImageTags() : null)}>
<FontAwesomeIcon icon="lightbulb" size="xl" />
</div>
</Tooltip>
{DocCast(this.Document.embedContainer)?.type_collection === CollectionViewType.Carousel ? null : (
<div>
<Tooltip title={<div>Create a flashcard pile</div>}>
- <div style={{ position: 'absolute', bottom: '3px', right: '74px', cursor: 'pointer' }} onPointerDown={e => this.createFlashcardPile([this.Document], false)}>
+ <div style={{ position: 'absolute', bottom: '3px', right: '74px', cursor: 'pointer' }} onPointerDown={() => this.createFlashcardPile([this.Document], false)}>
<FontAwesomeIcon icon="folder-plus" size="xl" />
</div>
</Tooltip>
<Tooltip title={<div className="dash-tooltip">Create new flashcard stack based on text</div>}>
- <div style={{ position: 'absolute', bottom: '3px', right: '104px', cursor: 'pointer' }} onClick={e => this.gptFlashcardPile()}>
+ <div style={{ position: 'absolute', bottom: '3px', right: '104px', cursor: 'pointer' }} onClick={() => this.gptFlashcardPile()}>
<FontAwesomeIcon icon="layer-group" size="xl" />
</div>
</Tooltip>
</div>
)}
<Tooltip title={<div className="dash-tooltip">Hover to reveal</div>}>
- <div style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }} onClick={e => this.handleHover()}>
+ <div style={{ position: 'absolute', bottom: '3px', right: '25px', cursor: 'pointer' }} onClick={() => this.handleHover()}>
<FontAwesomeIcon color={this.revealOp === 'hover' ? 'blue' : 'black'} icon="hand-point-up" size="xl" />
</div>
</Tooltip>
@@ -147,40 +124,37 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
@action handleHover = () => {
if (this.revealOp === 'hover') {
- this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'flip';
+ this.revealOp = 'flip';
this.Document.forceActive = false;
} else {
- this.layoutDoc[`_${this._props.fieldKey}_revealOp`] = 'hover';
+ this.revealOp = 'hover';
this.Document.forceActive = true;
}
};
@action handleInputChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
this._inputValue = e.target.value;
- console.log(this._inputValue);
};
@action activateContent = () => {
- this.childActive = true;
+ this._childActive = true;
};
@action handleRenderClick = () => {
- this.frontSide = !this.frontSide;
+ this._frontSide = !this._frontSide;
};
@action handleRenderGPTClick = async () => {
const phonTrans = DocCast(this.Document.audio) ? DocCast(this.Document.audio).phoneticTranscription : undefined;
if (phonTrans) {
this._inputValue = StrCast(phonTrans);
- console.log('INPUT:' + this._inputValue);
this.askGPTPhonemes(this._inputValue);
} else if (this._inputValue) this.askGPT(GPTCallType.QUIZ);
- this.frontSide = false;
+ this._frontSide = false;
this._outputValue = '';
};
- @action
- private onPointerMove = ({ movementX }: PointerEvent) => {
+ onPointerMove = ({ movementX }: PointerEvent) => {
const width = movementX * this.ScreenToLocalBoxXf().Scale + (this.clipWidth / 100) * this._props.PanelWidth();
if (width && width > 5 && width < this._props.PanelWidth()) {
this.layoutDoc[this.clipWidthKey] = (width * 100) / this._props.PanelWidth();
@@ -190,12 +164,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
componentDidMount() {
this._props.setContentViewBox?.(this);
- reaction(
+ this._reactDisposer = reaction(
() => this._props.isSelected(), // when this reaction should update
- selected => !selected && (this.childActive = false) // what it should update to
+ selected => !selected && (this._childActive = false) // what it should update to
);
}
+ componentWillUnmount() {
+ this._reactDisposer?.();
+ }
+
protected createDropTarget = (ele: HTMLDivElement | null, fieldKey: string, disposerId: number) => {
this._disposers[disposerId]?.();
if (ele) {
@@ -248,7 +226,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
this.dataDoc[which] = undefined;
return true;
}
- console.log('FALSE');
return false;
};
@@ -278,7 +255,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
remDoc1 = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_1'), true);
remDoc2 = (docs: Doc | Doc[]) => toList(docs).reduce((res, doc) => res && this.remDoc(doc, this.fieldKey + '_2'), true);
- private registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => {
+ registerSliding = (e: React.PointerEvent<HTMLDivElement>, targetWidth: number) => {
if (e.button !== 2) {
setupMoveUpEvents(
this,
@@ -287,7 +264,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
emptyFunction,
action((moveEv, doubleTap) => {
if (doubleTap) {
- this.childActive = true;
+ this._childActive = true;
if (!this.dataDoc[this.fieldKey + '_1'] && !this.dataDoc[this.fieldKey]) this.dataDoc[this.fieldKey + '_1'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
if (!this.dataDoc[this.fieldKey + '_2'] && !this.dataDoc[this.fieldKey + '_alternate']) this.dataDoc[this.fieldKey + '_2'] = DocUtils.copyDragFactory(Doc.UserDoc().emptyNote as Doc);
}
@@ -295,7 +272,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
false,
undefined,
action(() => {
- if (this.childActive) return;
+ if (this._childActive) return;
this._animating = 'all 200ms';
// on click, animate slider movement to the targetWidth
this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this._props.PanelWidth();
@@ -315,8 +292,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
* Set up speech to text tool.
*/
setListening = () => {
- const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
- if (SpeechRecognition) {
+ if (this.SpeechRecognition) {
this.recognition.continuous = true;
this.recognition.interimResults = true;
this.recognition.lang = 'en-US';
@@ -351,20 +327,20 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
case 'fr-FR': return 'French'; //prettier-ignore
case 'it-IT': return 'Italian'; //prettier-ignore
case 'zh-CH': return 'Mandarin Chinese'; //prettier-ignore
- case 'ja': return 'Japanese'; //prettier-ignore
- default: return 'Korean'; //prettier-ignore
+ case 'ja': return 'Japanese'; //prettier-ignore
+ default: return 'Korean'; //prettier-ignore
}
};
openContextMenu = (x: number, y: number, evalu: boolean) => {
ContextMenu.Instance.clearItems();
- ContextMenu.Instance.addItem({ description: 'English', event: e => this.setLanguage('en-US', 0), icon: 'question' }); //prettier-ignore
- ContextMenu.Instance.addItem({ description: 'Spanish', event: e => this.setLanguage('es-ES', 1 ), icon: 'question'}); //prettier-ignore
- ContextMenu.Instance.addItem({ description: 'French', event: e => this.setLanguage('fr-FR', 2), icon: 'question' }); //prettier-ignore
- ContextMenu.Instance.addItem({ description: 'Italian', event: e => this.setLanguage('it-IT', 3), icon: 'question' }); //prettier-ignore
- if (!evalu) ContextMenu.Instance.addItem({ description: 'Mandarin Chinese', event: e => this.setLanguage('zh-CH', 4), icon: 'question' }); //prettier-ignore
- ContextMenu.Instance.addItem({ description: 'Japanese', event: e => this.setLanguage('ja', 5), icon: 'question' }); //prettier-ignore
- ContextMenu.Instance.addItem({ description: 'Korean', event: e => this.setLanguage('ko', 6), icon: 'question' }); //prettier-ignore
+ ContextMenu.Instance.addItem({ description: 'English', event: () => this.setLanguage('en-US', 0), icon: 'question' }); //prettier-ignore
+ ContextMenu.Instance.addItem({ description: 'Spanish', event: () => this.setLanguage('es-ES', 1 ), icon: 'question'}); //prettier-ignore
+ ContextMenu.Instance.addItem({ description: 'French', event: () => this.setLanguage('fr-FR', 2), icon: 'question' }); //prettier-ignore
+ ContextMenu.Instance.addItem({ description: 'Italian', event: () => this.setLanguage('it-IT', 3), icon: 'question' }); //prettier-ignore
+ if (!evalu) ContextMenu.Instance.addItem({ description: 'Mandarin Chinese', event: () => this.setLanguage('zh-CH', 4), icon: 'question' }); //prettier-ignore
+ ContextMenu.Instance.addItem({ description: 'Japanese', event: () => this.setLanguage('ja', 5), icon: 'question' }); //prettier-ignore
+ ContextMenu.Instance.addItem({ description: 'Korean', event: () => this.setLanguage('ko', 6), icon: 'question' }); //prettier-ignore
ContextMenu.Instance.displayMenu(x, y);
};
@@ -430,7 +406,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
newCol['x'] = this.layoutDoc['x'];
newCol['y'] = NumCast(this.layoutDoc['y']) + 50;
newCol.type_collection = 'carousel';
- for (var i = 0; i < collectionArr.length; i++) {
+ for (let i = 0; i < collectionArr.length; i++) {
DocCast(collectionArr[i])[DocData].embedContainer = newCol;
}
@@ -448,10 +424,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
* Transfers the content of flashcards into a flashcard pile.
*/
gptFlashcardPile = async () => {
- var text = await this.askGPT(GPTCallType.STACK);
- var senArr = text?.split('Question: ');
- var collectionArr: Doc[] = [];
- for (let i = 1; i < senArr?.length!; i++) {
+ const text = await this.askGPT(GPTCallType.STACK);
+ const senArr = text?.split('Question: ') ?? [];
+ const collectionArr: Doc[] = [];
+ for (let i = 1; i < senArr.length; i++) {
const newDoc = Docs.Create.ComparisonDocument(senArr![i], { _layout_isFlashcard: true, _width: 300, _height: 300 });
if (StrCast(senArr![i]).includes('Keyword: ')) {
@@ -493,11 +469,10 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
* Calls GPT for each flashcard type.
*/
askGPT = async (callType: GPTCallType): Promise<string | undefined> => {
- let questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
+ const questionText = 'Question: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text);
const rubricText = ' Rubric: ' + StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_0']).text)?.Text);
const queryText = questionText + ' UserAnswer: ' + this._inputValue + '. ' + rubricText;
this._loading = true;
- const doc = DocCast(this.dataDoc[this.props.fieldKey + '_0']);
if (callType == GPTCallType.CHATCARD) {
if (StrCast(RTFCast(DocCast(this.dataDoc[this.fieldKey + '_1']).text)?.Text) === '') {
@@ -517,7 +492,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = res;
} else if (callType == GPTCallType.QUIZ) {
console.log(this._inputValue);
- this.frontSide = true;
+ this._frontSide = true;
this._outputValue = res.replace(/UserAnswer/g, "user's answer").replace(/Rubric/g, 'rubric');
} else if (callType === GPTCallType.FLASHCARD) {
this._loading = false;
@@ -526,7 +501,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
this._loading = false;
return res;
} catch (err) {
- console.error('GPT call failed');
+ console.error('GPT call failed', err);
}
this._loading = false;
};
@@ -538,7 +513,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
if (c?.length === 0) await this.askGPT(GPTCallType.CHATCARD);
if (c) {
this._loading = true;
- for (let i of c) {
+ for (const i of c) {
console.log(i);
if (i.className !== 'ProseMirror-separator') await this.getImageDesc(i.src);
}
@@ -664,17 +639,17 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
DocCast(this.dataDoc[this.props.fieldKey + '_0'])[DocData].text = response;
} catch (error) {
- console.log('Error');
+ console.log('Error', error);
}
};
@action
flipFlashcard = () => {
- this.frontSide = !this.frontSide;
+ this._frontSide = !this._frontSide;
};
hoverFlip = (side: boolean) => {
- if (this.revealOp === 'hover') this.frontSide = side;
+ if (this.revealOp === 'hover') this._frontSide = side;
};
testForTextFields = (whichSlot: string) => {
@@ -703,6 +678,8 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
return layoutTemplateString;
};
+ childActiveFunc = () => this._childActive;
+
render() {
const clearButton = (which: string) => (
<Tooltip title={<div className="dash-tooltip">remove</div>}>
@@ -723,10 +700,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
return targetDoc || layoutString ? (
<>
<DocumentView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
showTags={undefined}
- fitWidth={undefined}
+ fitWidth={undefined} // set to returnTrue to make images fill the comparisonBox-- should be a user option
ignoreUsePath={layoutString ? true : undefined}
renderDepth={this.props.renderDepth + 1}
LayoutTemplateString={layoutString}
@@ -734,15 +710,15 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
containerViewPath={this._props.docViewPath}
moveDocument={whichSlot.endsWith('1') ? this.moveDoc1 : this.moveDoc2}
removeDocument={whichSlot.endsWith('1') ? this.remDoc1 : this.remDoc2}
- NativeWidth={this.layoutWidth}
- NativeHeight={this.layoutHeight}
- isContentActive={() => this.childActive}
+ NativeWidth={returnZero}
+ NativeHeight={returnZero}
+ isContentActive={this.childActiveFunc}
isDocumentActive={returnFalse}
dontSelect={returnTrue}
whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- styleProvider={this.childActive ? this._props.styleProvider : this.docStyleProvider}
+ styleProvider={this._childActive ? this._props.styleProvider : this.docStyleProvider}
hideLinkButton
- pointerEvents={this.childActive ? undefined : returnNone}
+ pointerEvents={this._childActive ? undefined : returnNone}
/>
{!this.Document._layout_isFlashcard ? clearButton(whichSlot) : null}
</>
@@ -759,7 +735,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
style={{ width: this._props.PanelWidth() }}
onPointerDown={e => {
this.registerSliding(e, cover);
- this.activateContent();
+ this.Document._layout_isFlashcard && this.activateContent();
}}
ref={ele => this.createDropTarget(ele, which, index)}>
{!this._isEmpty ? displayDoc(which) : null}
@@ -767,7 +743,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
);
if (this.Document._layout_isFlashcard) {
- const side = this.frontSide ? 1 : 0;
+ const side = this._frontSide ? 1 : 0;
// add text box to each side when comparison box is first created
if (!this.dataDoc[this.fieldKey + '_0'] && !this._isEmpty) {
@@ -790,14 +766,14 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
<p style={{ display: text === '' ? 'flex' : 'none', color: 'white', marginLeft: '10px' }}>Return to all flashcards and add text to both sides. </p>
<div className="input-box">
<textarea
- value={this.frontSide ? this._outputValue : this._inputValue}
+ value={this._frontSide ? this._outputValue : this._inputValue}
onChange={this.handleInputChange}
onScroll={e => {
e.stopPropagation();
e.preventDefault();
}}
placeholder={!this.layoutDoc[`_${this._props.fieldKey}_usePath`] ? 'Enter a response for GPT to evaluate.' : ''}
- readOnly={this.frontSide}></textarea>
+ readOnly={this._frontSide}></textarea>
{this._loading ? (
<div className="loading-spinner" style={{ position: 'absolute' }}>
@@ -829,7 +805,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
Evaluate Pronunciation
</button>
- {!this.frontSide ? (
+ {!this._frontSide ? (
<button className="submit-buttonsubmit" type="button" onClick={this.handleRenderGPTClick} style={{ borderRadius: '2px', marginBottom: '3px', width: '100%' }}>
Submit
</button>
@@ -848,13 +824,9 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<FieldViewProps>()
return (
<div
className={`comparisonBox${this._props.isContentActive() ? '-interactive' : ''}`} /* change className to easily disable/enable pointer events in CSS */
- style={{ display: 'flex', flexDirection: 'column', overflow: 'hidden' }}
- onMouseEnter={() => {
- this.hoverFlip(false);
- }}
- onMouseLeave={() => {
- this.hoverFlip(true);
- }}>
+ style={{ display: 'flex', flexDirection: 'column' }}
+ onMouseEnter={() => this.hoverFlip(true)}
+ onMouseLeave={() => this.hoverFlip(false)}>
{displayBox(`${this.fieldKey}_${side === 0 ? 1 : 0}`, side, this._props.PanelWidth() - 3)}
{this._loading ? (
<div className="loading-spinner" style={{ position: 'absolute' }}>
diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
index 7179356b2..e57c9e842 100644
--- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx
+++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx
@@ -16,7 +16,6 @@ import { DocumentView } from '../../DocumentView';
import { DataVizView } from '../DataVizBox';
import './Chart.scss';
-// eslint-disable-next-line @typescript-eslint/no-var-requires
const { DATA_VIZ_TABLE_ROW_HEIGHT } = require('../../../global/globalCssVariables.module.scss'); // prettier-ignore
interface TableBoxProps {
diff --git a/src/client/views/nodes/DiagramBox.scss b/src/client/views/nodes/DiagramBox.scss
index 323638bff..8a7863c14 100644
--- a/src/client/views/nodes/DiagramBox.scss
+++ b/src/client/views/nodes/DiagramBox.scss
@@ -1,5 +1,3 @@
-$searchbarHeight: 50px;
-
.DIYNodeBox {
width: 100%;
height: 100%;
@@ -23,7 +21,6 @@ $searchbarHeight: 50px;
justify-content: center;
align-items: center;
width: 100%;
- height: $searchbarHeight;
padding: 10px;
input[type='text'] {
@@ -42,7 +39,6 @@ $searchbarHeight: 50px;
justify-content: center;
align-items: center;
width: 100%;
- height: calc(100% - $searchbarHeight);
.diagramBox {
flex: 1;
display: flex;
diff --git a/src/client/views/nodes/DiagramBox.tsx b/src/client/views/nodes/DiagramBox.tsx
index 36deb2d8d..d6c9bb013 100644
--- a/src/client/views/nodes/DiagramBox.tsx
+++ b/src/client/views/nodes/DiagramBox.tsx
@@ -18,7 +18,9 @@ import { InkingStroke } from '../InkingStroke';
import './DiagramBox.scss';
import { FieldView, FieldViewProps } from './FieldView';
import { FormattedTextBox } from './formattedText/FormattedTextBox';
-
+/**
+ * this is a class for the diagram box doc type that can be found in the tools section of the side bar
+ */
@observer
export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
public static LayoutString(fieldKey: string) {
@@ -59,14 +61,21 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
{ fireImmediately: true }
);
}
+ /**
+ * helper method for renderMermaidAsync
+ * @param str string containing the mermaid code
+ * @returns
+ */
renderMermaid = (str: string) => {
try {
return mermaid.render('graph' + Date.now(), str);
- } catch (error) {
+ } catch {
return { svg: '', bindFunctions: undefined };
}
};
-
+ /**
+ * will update the div containing the mermaid diagram to render the new mermaidCode
+ */
renderMermaidAsync = async (mermaidCode: string, dashDiv: HTMLDivElement) => {
try {
const { svg, bindFunctions } = await this.renderMermaid(mermaidCode);
@@ -97,7 +106,9 @@ export class DiagramBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
res
);
}, 'set mermaid code');
-
+ /**
+ * will generate mermaid code with GPT based on what the user requested
+ */
generateMermaidCode = action(() => {
this._generating = true;
const prompt = 'Write this in mermaid code and only give me the mermaid code: ' + this._inputValue;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index b85cb22bb..23ec42610 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -16,7 +16,7 @@ import { List } from '../../../fields/List';
import { PrefetchProxy } from '../../../fields/Proxy';
import { listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
-import { BoolCast, Cast, DocCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
+import { BoolCast, Cast, DocCast, ImageCast, NumCast, RTFCast, ScriptCast, StrCast } from '../../../fields/Types';
import { AudioField } from '../../../fields/URLField';
import { GetEffectiveAcl, TraceMobx } from '../../../fields/util';
import { AudioAnnoState } from '../../../server/SharedMediaTypes';
@@ -27,7 +27,7 @@ import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'
import { Docs } from '../../documents/Documents';
import { DragManager } from '../../util/DragManager';
import { dropActionType } from '../../util/DropActionTypes';
-import { MakeTemplate, makeUserTemplateButton } from '../../util/DropConverter';
+import { MakeTemplate, makeUserTemplateButtonOrImage } from '../../util/DropConverter';
import { UPDATE_SERVER_CACHE } from '../../util/LinkManager';
import { ScriptingGlobals } from '../../util/ScriptingGlobals';
import { SearchUtil } from '../../util/SearchUtil';
@@ -1091,10 +1091,21 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
* Pins a Doc to the current presentation trail. (see TabDocView for implementation)
*/
public static PinDoc: (docIn: Doc | Doc[], pinProps: PinProps) => void;
+
+ /**
+ * Renders an image of a Doc into the Doc's icon field, then returns a promise for the image value
+ * @param doc Doc to snapshot
+ * @returns promise of icon ImageField
+ */
+ public static GetDocImage(doc: Doc) {
+ return DocumentView.getDocumentView(doc)
+ ?.ComponentView?.updateIcon?.()
+ .then(() => ImageCast(DocCast(doc).icon));
+ }
/**
* The DocumentView below the cursor at the start of a gesture (that receives the pointerDown event). Used by GestureOverlay to determine the doc a gesture should apply to.
*/
- public static DownDocView: DocumentView | undefined;
+ public static DownDocView: DocumentView | undefined; // the first DocView that receives a pointerdown event. used by GestureOverlay to determine the doc a gesture should apply to.
public get displayName() { return 'DocumentView(' + (this.Document?.title??"") + ')'; } // prettier-ignore
private _htmlOverlayEffect: Opt<Doc>;
@@ -1340,7 +1351,7 @@ export class DocumentView extends DocComponent<DocumentViewProps>() {
tempDoc = view.Document;
MakeTemplate(tempDoc);
Doc.AddDocToList(Doc.UserDoc(), 'template_user', tempDoc);
- Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButton(tempDoc));
+ Doc.AddDocToList(DocListCast(Doc.MyTools.data)[1], 'data', makeUserTemplateButtonOrImage(tempDoc));
tempDoc && Doc.AddDocToList(Cast(Doc.UserDoc().template_user, Doc, null), 'data', tempDoc);
} else {
tempDoc = DocCast(view.Document[StrCast(view.Document.layout_fieldKey)]);
@@ -1589,7 +1600,7 @@ export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.acti
export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, '0'); } // prettier-ignore
export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } // prettier-ignore
export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } // prettier-ignore
-export function ActiveEraserWidth(): number { return Number(ActiveInkPen()?.eraserWidth); } // prettier-ignore
+export function ActiveEraserWidth(): number { return Number(ActiveInkPen()?.eraserWidth ?? 25); } // prettier-ignore
export function SetActiveInkWidth(width: string): void {
!isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width);
diff --git a/src/client/views/nodes/FontIconBox/FontIconBox.tsx b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
index cb0c4d188..feaf84b7b 100644
--- a/src/client/views/nodes/FontIconBox/FontIconBox.tsx
+++ b/src/client/views/nodes/FontIconBox/FontIconBox.tsx
@@ -1,4 +1,3 @@
-/* eslint-disable react/jsx-props-no-spreading */
import { IconProp } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Button, ColorPicker, Dropdown, DropdownType, IconButton, IListItemProps, MultiToggle, NumberDropdown, NumberDropdownType, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
@@ -262,9 +261,10 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
const tooltip: string = StrCast(this.Document.toolTip);
const script = ScriptCast(this.Document.onClick)?.script;
- const toggleStatus = script?.run({ this: this.Document, self: this.Document, value: undefined, _readOnly_: true }).result as boolean;
+ const toggleStatus = script?.run({ this: this.Document, value: undefined, _readOnly_: true }).result as boolean;
// Colors
const color = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.Color) as string;
+ const background = this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.BackgroundColor) as string;
const items = DocListCast(this.dataDoc.data);
const selectedItems = items.filter(itemDoc => ScriptCast(itemDoc.onClick).script.run({ this: itemDoc, value: undefined, _readOnly_: true }).result).map(item => StrCast(item.toolType));
return (
@@ -272,11 +272,11 @@ export class FontIconBox extends ViewBoxBaseComponent<ButtonProps>() {
tooltip={`Toggle ${tooltip}`}
type={Type.PRIM}
color={color}
+ background={background === SnappingManager.userBackgroundColor ? undefined : background}
multiSelect={true}
onPointerDown={e => script && !toggleStatus && setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => script.run({ this: this.Document, value: undefined, _readOnly_: false }))}
- isToggle={script ? true : false}
+ isToggle={false}
toggleStatus={toggleStatus}
- //background={SnappingManager.userBackgroundColor}
label={this.label}
items={items.map(item => ({
icon: <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={StrCast(item.icon) as IconProp} color={color} />,
diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx
index 32d1e471e..31f6df2ea 100644
--- a/src/client/views/nodes/ImageBox.tsx
+++ b/src/client/views/nodes/ImageBox.tsx
@@ -25,7 +25,7 @@ import { Networking } from '../../Network';
import { DragManager } from '../../util/DragManager';
import { dropActionType } from '../../util/DropActionTypes';
import { SnappingManager } from '../../util/SnappingManager';
-import { undoBatch } from '../../util/UndoManager';
+import { undoable, undoBatch } from '../../util/UndoManager';
import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
@@ -195,8 +195,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const images = await this.fetchImages();
};
- @undoBatch
- drop = (e: Event, de: DragManager.DropEvent) => {
+ drop = undoable((e: Event, de: DragManager.DropEvent) => {
if (de.complete.docDragData) {
let added: boolean | undefined;
const targetIsBullseye = (ele: HTMLElement): boolean => {
@@ -225,7 +224,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return added;
}
return false;
- };
+ }, 'image drop');
@undoBatch
resolution = () => {
@@ -316,15 +315,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return cropping;
};
- /**
- * Draw image to a canvas so it can be converted to base64 and be passed into
- * GPT to be analyzed.
- * @param downX
- * @param downY
- * @param cb
- * @returns
- */
- createCanvas = async (downX?: number, downY?: number, cb?: (filename: string, x: number | undefined, y: number | undefined) => void) => {
+ createCanvas = async () => {
const canvas = document.createElement('canvas');
const scaling = 1 / (this._props.NativeDimScaling?.() || 1);
const w = AnchorMenu.Instance.marqueeWidth * scaling;
@@ -407,9 +398,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
* @param texts
*/
createBoxes = (boxes: [[[number, number]]], texts: [string]) => {
- const nscale = NumCast(this._props.PanelWidth()) * NumCast(this.layoutDoc._freeform_scale, 1);
- const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']);
- for (var i = 0; i < boxes.length; i++) {
+ for (let i = 0; i < boxes.length; i++) {
const coords = boxes[i] ? boxes[i] : [];
const width = coords[1][0] - coords[0][0];
const height = coords[2][1] - coords[0][1];
@@ -446,7 +435,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const response = await gptImageLabel(hrefBase64, 'Make flashcards out of this image with each question and answer labeled as "question" and "answer". Do not label each flashcard and do not include asterisks: ');
AnchorMenu.Instance.transferToFlashcard(response, NumCast(this.layoutDoc['x']), NumCast(this.layoutDoc['y']));
} catch (error) {
- console.log('Error');
+ console.log('Error', error);
}
this._loading = false;
};
@@ -461,7 +450,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const hrefBase64 = await this.createCanvas();
this.pushInfo(quizMode.NORMAL, hrefBase64);
} catch (error) {
- console.log('Error');
+ console.log('Error', error);
}
};
@@ -877,7 +866,6 @@ export class ImageBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}}>
<CollectionFreeFormView
ref={this._ffref}
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
setContentViewBox={emptyFunction}
NativeWidth={returnZero}
diff --git a/src/client/views/nodes/LabelBigText.js b/src/client/views/nodes/LabelBigText.js
deleted file mode 100644
index 290152cd0..000000000
--- a/src/client/views/nodes/LabelBigText.js
+++ /dev/null
@@ -1,270 +0,0 @@
-/*
-Brorlandi/big-text.js v1.0.0, 2017
-Adapted from DanielHoffmann/jquery-bigtext, v1.3.0, May 2014
-And from Jetroid/bigtext.js v1.0.0, September 2016
-
-Usage:
-BigText("#myElement",{
- rotateText: {Number}, (null)
- fontSizeFactor: {Number}, (0.8)
- maximumFontSize: {Number}, (null)
- limitingDimension: {String}, ("both")
- horizontalAlign: {String}, ("center")
- verticalAlign: {String}, ("center")
- textAlign: {String}, ("center")
- whiteSpace: {String}, ("nowrap")
-});
-
-
-Original Projects:
-https://github.com/DanielHoffmann/jquery-bigtext
-https://github.com/Jetroid/bigtext.js
-
-Options:
-
-rotateText: Rotates the text inside the element by X degrees.
-
-fontSizeFactor: This option is used to give some vertical spacing for letters that overflow the line-height (like 'g', 'Á' and most other accentuated uppercase letters). This does not affect the font-size if the limiting factor is the width of the parent div. The default is 0.8
-
-maximumFontSize: maximum font size to use.
-
-minimumFontSize: minimum font size to use. if font is calculated smaller than this, text will be rendered at this size and wrapped
-
-limitingDimension: In which dimension the font size should be limited. Possible values: "width", "height" or "both". Defaults to both. Using this option with values different than "both" overwrites the element parent width or height.
-
-horizontalAlign: Where to align the text horizontally. Possible values: "left", "center", "right". Defaults to "center".
-
-verticalAlign: Where to align the text vertically. Possible values: "top", "center", "bottom". Defaults to "center".
-
-textAlign: Sets the text align of the element. Possible values: "left", "center", "right". Defaults to "center". This option is only useful if there are linebreaks (<br> tags) inside the text.
-
-whiteSpace: Sets whitespace handling. Possible values: "nowrap", "pre". Defaults to "nowrap". (Can also be set to enable wrapping but this doesn't work well.)
-
-Bruno Orlandi - 2017
-
-Copyright (C) 2013 Daniel Hoffmann Bernardes, Ícaro Technologies
-Copyright (C) 2016 Jet Holt
-
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
-
-The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
-
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-*/
-
-function _calculateInnerDimensions(computedStyle) {
- //Calculate the inner width and height
- var innerWidth;
- var innerHeight;
-
- var width = parseInt(computedStyle.getPropertyValue("width"));
- var height = parseInt(computedStyle.getPropertyValue("height"));
- var paddingLeft = parseInt(computedStyle.getPropertyValue("padding-left"));
- var paddingRight = parseInt(computedStyle.getPropertyValue("padding-right"));
- var paddingTop = parseInt(computedStyle.getPropertyValue("padding-top"));
- var paddingBottom = parseInt(computedStyle.getPropertyValue("padding-bottom"));
- var borderLeft = parseInt(computedStyle.getPropertyValue("border-left-width"));
- var borderRight = parseInt(computedStyle.getPropertyValue("border-right-width"));
- var borderTop = parseInt(computedStyle.getPropertyValue("border-top-width"));
- var borderBottom = parseInt(computedStyle.getPropertyValue("border-bottom-width"));
-
- //If box-sizing is border-box, we need to subtract padding and border.
- var parentBoxSizing = computedStyle.getPropertyValue("box-sizing");
- if (parentBoxSizing == "border-box") {
- innerWidth = width - (paddingLeft + paddingRight + borderLeft + borderRight);
- innerHeight = height - (paddingTop + paddingBottom + borderTop + borderBottom);
- } else {
- innerWidth = width;
- innerHeight = height;
- }
- var obj = {};
- obj["width"] = innerWidth;
- obj["height"] = innerHeight;
- return obj;
-}
-
-export default function BigText(element, options) {
-
- if (typeof element === 'string') {
- element = document.querySelector(element);
- } else if (element.length) {
- // Support for array based queries (such as jQuery)
- element = element[0];
- }
-
- var defaultOptions = {
- rotateText: null,
- fontSizeFactor: 0.8,
- maximumFontSize: null,
- limitingDimension: "both",
- horizontalAlign: "center",
- verticalAlign: "center",
- textAlign: "center",
- whiteSpace: "nowrap",
- singleLine: true
- };
-
- //Merge provided options and default options
- options = options || {};
- for (var opt in defaultOptions)
- if (defaultOptions.hasOwnProperty(opt) && !options.hasOwnProperty(opt))
- options[opt] = defaultOptions[opt];
-
- //Get variables which we will reference frequently
- var style = element.style;
- var parent = element.parentNode;
- var parentStyle = parent.style;
- var parentComputedStyle = document.defaultView.getComputedStyle(parent);
-
- //hides the element to prevent "flashing"
- style.visibility = "hidden";
- //Set some properties
- style.display = "inline-block";
- style.clear = "both";
- style.float = "left";
- var fontSize = options.maximumFontSize;
- if (options.singleLine) {
- style.fontSize = (fontSize * options.fontSizeFactor) + "px";
- style.lineHeight = fontSize + "px";
- } else {
- for (; fontSize > options.minimumFontSize; fontSize = fontSize - Math.min(fontSize / 2, Math.max(0, fontSize - 48) + 2)) {
- style.fontSize = (fontSize * options.fontSizeFactor) + "px";
- style.lineHeight = "1";
- if (element.offsetHeight <= +parentComputedStyle.height.replace("px", "")) {
- break;
- }
- }
- }
- style.whiteSpace = options.whiteSpace;
- style.textAlign = options.textAlign;
- style.position = "relative";
- style.padding = 0;
- style.margin = 0;
- style.left = "50%";
- style.top = "50%";
- var computedStyle = document.defaultView.getComputedStyle(element);
-
- //Get properties of parent to allow easier referencing later.
- var parentPadding = {
- top: parseInt(parentComputedStyle.getPropertyValue("padding-top")),
- right: parseInt(parentComputedStyle.getPropertyValue("padding-right")),
- bottom: parseInt(parentComputedStyle.getPropertyValue("padding-bottom")),
- left: parseInt(parentComputedStyle.getPropertyValue("padding-left")),
- };
- var parentBorder = {
- top: parseInt(parentComputedStyle.getPropertyValue("border-top")),
- right: parseInt(parentComputedStyle.getPropertyValue("border-right")),
- bottom: parseInt(parentComputedStyle.getPropertyValue("border-bottom")),
- left: parseInt(parentComputedStyle.getPropertyValue("border-left")),
- };
-
- //Calculate the parent inner width and height
- var parentInnerDimensions = _calculateInnerDimensions(parentComputedStyle);
- var parentInnerWidth = parentInnerDimensions["width"];
- var parentInnerHeight = parentInnerDimensions["height"];
-
- var box = {
- width: element.offsetWidth, //Note: This is slightly larger than the jQuery version
- height: element.offsetHeight,
- };
- if (!box.width || !box.height) return element;
-
-
- if (options.rotateText !== null) {
- if (typeof options.rotateText !== "number")
- throw "bigText error: rotateText value must be a number";
- var rotate = "rotate(" + options.rotateText + "deg)";
- style.webkitTransform = rotate;
- style.msTransform = rotate;
- style.MozTransform = rotate;
- style.OTransform = rotate;
- style.transform = rotate;
- //calculating bounding box of the rotated element
- var sine = Math.abs(Math.sin(options.rotateText * Math.PI / 180));
- var cosine = Math.abs(Math.cos(options.rotateText * Math.PI / 180));
- box.width = element.offsetWidth * cosine + element.offsetHeight * sine;
- box.height = element.offsetWidth * sine + element.offsetHeight * cosine;
- }
-
- var parentWidth = (parentInnerWidth - parentPadding.left - parentPadding.right);
- var parentHeight = (parentInnerHeight - parentPadding.top - parentPadding.bottom);
- var widthFactor = parentWidth / box.width;
- var heightFactor = parentHeight / box.height;
- var lineHeight;
-
- if (options.limitingDimension.toLowerCase() === "width") {
- lineHeight = Math.floor(widthFactor * fontSize);
- } else if (options.limitingDimension.toLowerCase() === "height") {
- lineHeight = Math.floor(heightFactor * fontSize);
- } else if (widthFactor < heightFactor)
- lineHeight = Math.floor(widthFactor * fontSize);
- else if (widthFactor >= heightFactor)
- lineHeight = Math.floor(heightFactor * fontSize);
-
- var fontSize = lineHeight * options.fontSizeFactor;
- if (fontSize < options.minimumFontSize) {
- parentStyle.display = "flex";
- parentStyle.alignItems = "center";
- style.textAlign = "center";
- style.visibility = "";
- style.fontSize = options.minimumFontSize + "px";
- style.lineHeight = "";
- style.overflow = "hidden";
- style.textOverflow = "ellipsis";
- style.top = "";
- style.left = "";
- style.margin = "";
- return element;
- }
- if (options.maximumFontSize && fontSize > options.maximumFontSize) {
- fontSize = options.maximumFontSize;
- lineHeight = fontSize / options.fontSizeFactor;
- }
-
- style.fontSize = Math.floor(fontSize) + "px";
- style.lineHeight = Math.ceil(lineHeight) + "px";
- style.marginBottom = "0px";
- style.marginRight = "0px";
-
- // if (options.limitingDimension.toLowerCase() === "height") {
- // //this option needs the font-size to be set already so computedStyle.getPropertyValue("width") returns the right size
- // //this +4 is to compensate the rounding erros that can occur due to the calls to Math.floor in the centering code
- // parentStyle.width = (parseInt(computedStyle.getPropertyValue("width")) + 4) + "px";
- // }
-
- //Calculate the inner width and height
- var innerDimensions = _calculateInnerDimensions(computedStyle);
- var innerWidth = innerDimensions["width"];
- var innerHeight = innerDimensions["height"];
-
- switch (options.verticalAlign.toLowerCase()) {
- case "top":
- style.top = "0%";
- break;
- case "bottom":
- style.top = "100%";
- style.marginTop = Math.floor(-innerHeight) + "px";
- break;
- default:
- style.marginTop = Math.ceil((-innerHeight / 2)) + "px";
- break;
- }
-
- switch (options.horizontalAlign.toLowerCase()) {
- case "left":
- style.left = "0%";
- break;
- case "right":
- style.left = "100%";
- style.marginLeft = Math.floor(-innerWidth) + "px";
- break;
- default:
- style.marginLeft = Math.ceil((-innerWidth / 2)) + "px";
- break;
- }
-
- //shows the element after the work is done
- style.visibility = "visible";
-
- return element;
-}
diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx
index 79366f76f..058932457 100644
--- a/src/client/views/nodes/LabelBox.tsx
+++ b/src/client/views/nodes/LabelBox.tsx
@@ -138,7 +138,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps>() {
};
if (r) {
if (!r.offsetHeight || !r.offsetWidth) {
- console.log("CAN'T FIT TO EMPTY BOX");
+ //console.log("CAN'T FIT TO EMPTY BOX");
this._timeout && clearTimeout(this._timeout);
this._timeout = setTimeout(() => this.fitTextToBox(r));
return textfitParams;
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index e7470c74c..816d4a3b0 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -10,7 +10,7 @@ import { DocData } from '../../../fields/DocSymbols';
import { Id } from '../../../fields/FieldSymbols';
import { InkTool } from '../../../fields/InkField';
import { ComputedField } from '../../../fields/ScriptField';
-import { Cast, FieldValue, ImageCast, NumCast, StrCast, toList } from '../../../fields/Types';
+import { Cast, FieldValue, NumCast, StrCast, toList } from '../../../fields/Types';
import { ImageField, PdfField } from '../../../fields/URLField';
import { TraceMobx } from '../../../fields/util';
import { emptyFunction } from '../../../Utils';
@@ -173,28 +173,26 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
updateIcon = () => {
// currently we render pdf icons as text labels
const docViewContent = this.DocumentView?.().ContentDiv;
- const filename = this.layoutDoc[Id] + '-icon' + new Date().getTime();
- this._pdfViewer?._mainCont.current &&
- docViewContent &&
- UpdateIcon(
- filename,
- docViewContent,
- NumCast(this.layoutDoc._width),
- NumCast(this.layoutDoc._height),
- this._props.PanelWidth(),
- this._props.PanelHeight(),
- NumCast(this.layoutDoc._layout_scrollTop),
- NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], 1),
- true,
- this.layoutDoc[Id] + '-icon',
- (iconFile: string, nativeWidth: number, nativeHeight: number) => {
- setTimeout(() => {
- this.dataDoc.icon = new ImageField(iconFile);
- this.dataDoc.icon_nativeWidth = nativeWidth;
- this.dataDoc.icon_nativeHeight = nativeHeight;
- }, 500);
- }
- );
+ const filename = this.layoutDoc[Id] + '_icon_' + new Date().getTime();
+ return !(this._pdfViewer?._mainCont.current && docViewContent)
+ ? new Promise<void>(res => res())
+ : UpdateIcon(
+ filename,
+ docViewContent,
+ NumCast(this.layoutDoc._width),
+ NumCast(this.layoutDoc._height),
+ this._props.PanelWidth(),
+ this._props.PanelHeight(),
+ NumCast(this.layoutDoc._layout_scrollTop),
+ NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], 1),
+ true,
+ this.layoutDoc[Id] + '_icon_' + new Date().getTime(),
+ (iconFile: string, nativeWidth: number, nativeHeight: number) => {
+ this.dataDoc.icon = new ImageField(iconFile);
+ this.dataDoc.icon_nativeWidth = nativeWidth;
+ this.dataDoc.icon_nativeHeight = nativeHeight;
+ }
+ );
};
componentWillUnmount() {
@@ -537,7 +535,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
return ComponentTag === CollectionStackingView ? (
<SidebarAnnos
ref={this._sidebarRef}
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
Document={this.Document}
layoutDoc={this.layoutDoc}
@@ -553,7 +550,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
) : (
<div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => this._props.select(false), true)}>
<ComponentTag
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
setContentViewBox={emptyFunction} // override setContentView to do nothing
NativeWidth={this.sidebarNativeWidthFunc}
@@ -607,7 +603,6 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
top: 0,
}}>
<PDFViewer
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
pdfBox={this}
sidebarAddDoc={this.sidebarAddDocument}
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 4933869a7..d653b27d7 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -298,18 +298,19 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const retitled = StrCast(this.Document.title).replace(/[ -.:]/g, '');
const encodedFilename = encodeURIComponent(('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString()).replace(/[./?=]/g, '_'));
const filename = basename(encodedFilename);
- ClientUtils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY));
+ return ClientUtils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => {
+ if (returnedFilename) (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY);
+ });
}
+ return new Promise<void>(res => res());
};
- updateIcon = () => {
- const makeIcon = (returnedfilename: string) => {
+ updateIcon = () =>
+ this.Snapshot(undefined, undefined, (returnedfilename: string) => {
this.dataDoc.icon = new ImageField(returnedfilename);
this.dataDoc.icon_nativeWidth = NumCast(this.layoutDoc._width);
this.dataDoc.icon_nativeHeight = NumCast(this.layoutDoc._height);
- };
- this.Snapshot(undefined, undefined, makeIcon);
- };
+ });
// creates link for snapshot
createSnapshotLink = (imagePath: string, downX?: number, downY?: number) => {
@@ -459,7 +460,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
const url = field.url.href;
const subitems: ContextMenuProps[] = [];
subitems.push({ description: 'Full Screen', event: this.FullScreen, icon: 'expand' });
- subitems.push({ description: 'Take Snapshot', event: this.Snapshot, icon: 'expand-arrows-alt' });
+ subitems.push({ description: 'Take Snapshot', event: () => this.Snapshot(), icon: 'expand-arrows-alt' });
this.Document.type === DocumentType.SCREENSHOT &&
subitems.push({
description: 'Screen Capture',
diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx
index 1fd73c226..a5788d02a 100644
--- a/src/client/views/nodes/WebBox.tsx
+++ b/src/client/views/nodes/WebBox.tsx
@@ -44,7 +44,6 @@ import { LinkInfo } from './LinkDocPreview';
import { OpenWhere } from './OpenWhere';
import './WebBox.scss';
-// eslint-disable-next-line @typescript-eslint/no-var-requires
const { CreateImage } = require('./WebBoxRenderer');
@observer
@@ -145,38 +144,31 @@ export class WebBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
};
updateIcon = async () => {
- if (!this._iframe) return;
+ if (!this._iframe) return new Promise<void>(res => res());
const scrollTop = NumCast(this.layoutDoc._layout_scrollTop);
const nativeWidth = NumCast(this.layoutDoc.nativeWidth);
const nativeHeight = (nativeWidth * this._props.PanelHeight()) / this._props.PanelWidth();
let htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument);
if (!htmlString) {
- htmlString = await (await fetch(ClientUtils.CorsProxy(this.webField!.href))).text();
+ htmlString = await fetch(ClientUtils.CorsProxy(this.webField!.href)).then(response => response.text());
}
this.layoutDoc.thumb = undefined;
this.Document.thumbLockout = true; // lock to prevent multiple thumb updates.
- CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop)
+ return (CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) as Promise<string>)
.then((dataUrl: string) => {
if (dataUrl.includes('<!DOCTYPE')) {
console.log('BAD DATA IN THUMB CREATION');
return;
}
- ClientUtils.convertDataUri(dataUrl, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename =>
- setTimeout(
- action(() => {
- this.Document.thumbLockout = false;
- this.layoutDoc.thumb = new ImageField(returnedfilename);
- this.layoutDoc.thumbScrollTop = scrollTop;
- this.layoutDoc.thumbNativeWidth = nativeWidth;
- this.layoutDoc.thumbNativeHeight = nativeHeight;
- }),
- 500
- )
- );
+ return ClientUtils.convertDataUri(dataUrl, this.layoutDoc[Id] + '_icon_' + new Date().getTime(), true, this.layoutDoc[Id] + '_icon_').then(returnedfilename => {
+ this.Document.thumbLockout = false;
+ this.layoutDoc.thumb = new ImageField(returnedfilename);
+ this.layoutDoc.thumbScrollTop = scrollTop;
+ this.layoutDoc.thumbNativeWidth = nativeWidth;
+ this.layoutDoc.thumbNativeHeight = nativeHeight;
+ });
})
- .catch((error: object) => {
- console.error('oops, something went wrong!', error);
- });
+ .catch((error: object) => console.error('oops, something went wrong!', error));
};
componentDidMount() {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index 07a4d1093..865146d68 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -141,6 +141,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
set _recordingDictation(value) {
!this.dataDoc[`${this.fieldKey}_recordingSource`] && (this.dataDoc.mediaState = value ? mediaState.Recording : undefined);
}
+
+ // eslint-disable-next-line no-return-assign
+ @computed get config() { return FormattedTextBox.MakeConfig(this._rules = new RichTextRules(this.Document, this), this._props); } // prettier-ignore
@computed get _recordingDictation() { return this.dataDoc?.mediaState === mediaState.Recording; } // prettier-ignore
@computed get SidebarShown() { return !!(this._showSidebar || this.layoutDoc._layout_showSidebar); } // prettier-ignore
@computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.sidebarKey]); } // prettier-ignore
@@ -153,7 +156,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
@computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.dataDoc[this.sidebarKey + '_height']); } // prettier-ignore
@computed get titleHeight() { return this._props.styleProvider?.(this.layoutDoc, this._props, StyleProp.HeaderMargin) as number || 0; } // prettier-ignore
@computed get layout_autoHeightMargins() { return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); } // prettier-ignore
- @computed get config() { return FormattedTextBox.MakeConfig(this._rules = new RichTextRules(this.Document, this), this._props); } // prettier-ignore
@computed get sidebarKey() { return this.fieldKey + '_sidebar'; } // prettier-ignore
constructor(props: FormattedTextBoxProps) {
@@ -229,6 +231,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
setupAnchorMenu = () => {
AnchorMenu.Instance.Status = 'marquee';
AnchorMenu.Instance.gptFlashcards = this.gptPDFFlashcards;
+ AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument;
AnchorMenu.Instance.OnClick = () => {
!this.layoutDoc.layout_showSidebar && this.toggleSidebar();
setTimeout(() => this._sidebarRef.current?.anchorMenuClick(this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true))); // give time for sidebarRef to be created
@@ -279,6 +282,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
docView && DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(docView, () => this.getAnchor(true), targetCreator), e.pageX, e.pageY);
});
+ AnchorMenu.Instance.AddDrawingAnnotation = (drawing: Doc) => {
+ const container = DocCast(this.Document.embedContainer);
+ const docView = DocumentView.getDocumentView?.(container);
+ docView?.ComponentView?._props.addDocument?.(drawing);
+ drawing.x = NumCast(this.Document.x) + NumCast(this.Document.width);
+ drawing.y = NumCast(this.Document.y);
+ };
+
AnchorMenu.Instance.setSelectedText(window.getSelection()?.toString() ?? '');
const coordsB = this._editorView!.coordsAtPos(this._editorView!.state.selection.to);
this._props.rootSelected?.() && AnchorMenu.Instance.jumpTo(coordsB.left, coordsB.bottom);
@@ -663,7 +674,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
textContent += node.child(i).textContent;
i++;
}
- // eslint-disable-next-line no-cond-assign
while (ep && (foundAt = textContent.slice(index).search(regexp)) > -1) {
const sel = new TextSelection(pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + 1), pm.state.doc.resolve(ep.from + index + blockOffset + foundAt + find.length + 1));
ret.push(sel);
@@ -722,7 +732,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const hr = Math.round(Date.now() / 1000 / 60 / 60);
numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() }));
}
- // eslint-disable-next-line operator-assignment
this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone interested in layout changes triggered by css changes (eg., CollectionLinkView)
};
@@ -987,7 +996,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
findImageTags = async () => {
const c = this.ProseRef?.getElementsByTagName('img');
if (c) {
- for (let i of c) {
+ for (const i of c) {
console.log(i);
// console.log(canvas.toDataURL());
@@ -1034,7 +1043,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
// this.Document[DocData].description = response.trim();
// return response; // Return the response
} catch (error) {
- console.log('Error');
+ console.log('Error', error);
}
// return '';
};
@@ -1195,7 +1204,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const nodes: Node[] = [];
let hadStart = start !== 0;
frag.forEach((node, index) => {
- // eslint-disable-next-line no-use-before-define
const examinedNode = findAnchorNode(node, editor);
if (examinedNode?.node && (examinedNode.node.textContent || examinedNode.node.type === this._editorView?.state.schema.nodes.dashDoc || examinedNode.node.type === this._editorView?.state.schema.nodes.audiotag)) {
nodes.push(examinedNode.node);
@@ -1261,6 +1269,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
if (nh) this.layoutDoc._nativeHeight = scrollHeight;
};
+ @computed get tagsHeight() {
+ return this.DocumentView?.().showTags ? Math.max(0, 20 - Math.max(this._props.yPadding ?? 0, NumCast(this.layoutDoc._yMargin))) : 0;
+ }
+
@computed get contentScaling() {
return Doc.NativeAspect(this.Document, this.dataDoc, false) ? this._props.NativeDimScaling?.() || 1 : 1;
}
@@ -1288,9 +1300,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
);
this._disposers.componentHeights = reaction(
// set the document height when one of the component heights changes and layout_autoHeight is on
- () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }),
- ({ sidebarHeight, textHeight, layoutAutoHeight, marginsHeight }) => {
- const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight));
+ () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layoutAutoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins, tagsHeight: this.tagsHeight }),
+ ({ sidebarHeight, textHeight, layoutAutoHeight, marginsHeight, tagsHeight }) => {
+ const newHeight = this.contentScaling * (tagsHeight + marginsHeight + Math.max(sidebarHeight, textHeight));
if (
(!Array.from(FormattedTextBox._globalHighlights).includes('Bold Text') || this._props.isSelected()) && //
layoutAutoHeight &&
@@ -1349,7 +1361,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
action(selected => {
this.prepareForTyping();
if (FormattedTextBox._globalHighlights.has('Bold Text')) {
- // eslint-disable-next-line operator-assignment
this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed
}
if (RichTextMenu.Instance?.view === this._editorView && !selected) {
@@ -1800,7 +1811,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const match = RTFCast(this.Document[this.fieldKey])?.Text.match(/^(@[a-zA-Z][a-zA-Z_0-9 -]*[a-zA-Z_0-9-]+)/);
if (match) {
this.dataDoc.title_custom = true;
- // eslint-disable-next-line prefer-destructuring
this.dataDoc.title = match[1]; // this triggers the collectionDockingView to publish this Doc
this.EditorView?.dispatch(this.EditorView?.state.tr.deleteRange(0, match[1].length + 1));
}
@@ -1882,7 +1892,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const margins = 2 * NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined;
if (children && !SnappingManager.IsDragging) {
- // eslint-disable-next-line no-use-before-define
const getChildrenHeights = (kids: Element[] | undefined) => kids?.reduce((p, child) => p + toHgt(child), margins) ?? 0;
const toNum = (val: string) => Number(val.replace('px', ''));
const toHgt = (node: Element): number => {
@@ -1969,7 +1978,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
return ComponentTag === CollectionStackingView ? (
<SidebarAnnos
ref={this._sidebarRef}
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
Document={this.Document}
layoutDoc={this.layoutDoc}
@@ -1989,7 +1997,6 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
) : (
<div onPointerDown={e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, () => DocumentView.SelectView(this.DocumentView?.(), false), true)}>
<ComponentTag
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
ref={this._sidebarTagRef}
setContentView={emptyFunction}
@@ -2098,8 +2105,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
const scale = this._props.NativeDimScaling?.() || 1;
const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : '';
setTimeout(() => !this._props.isContentActive() && FormattedTextBoxComment.textBox === this && FormattedTextBoxComment.Hide);
- const paddingX = NumCast(this.layoutDoc._xMargin, this._props.xPadding || 0);
- const paddingY = NumCast(this.layoutDoc._yMargin, this._props.yPadding || 0);
+ const paddingX = Math.max(this._props.xPadding ?? 0, NumCast(this.layoutDoc._xMargin));
+ const paddingY = Math.max(this._props.yPadding ?? 0, NumCast(this.layoutDoc._yMargin));
const styleFromLayout = styleFromLayoutString(this.Document, this._props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._header_height}px' >
return styleFromLayout?.height === '0px' ? null : (
<div
@@ -2118,6 +2125,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FormattedTextB
height: `${100 / scale}%`,
}),
transition: 'inherit',
+ paddingBottom: this.tagsHeight,
// overflowY: this.layoutDoc._layout_autoHeight ? "hidden" : undefined,
color: this.fontColor,
fontSize: this.fontSize,
diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx
index 3ec799836..88e2e4248 100644
--- a/src/client/views/nodes/formattedText/RichTextMenu.tsx
+++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx
@@ -68,10 +68,12 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> {
constructor(props: AntimodeMenuProps) {
super(props);
makeObservable(this);
- runInAction(() => (RichTextMenu._instance.menu = this));
- this.updateMenu(undefined, undefined, props, this.layoutDoc);
- this._canFade = false;
- this.Pinned = true;
+ runInAction(() => {
+ RichTextMenu._instance.menu = this;
+ this.updateMenu(undefined, undefined, props, this.layoutDoc);
+ this._canFade = false;
+ this.Pinned = true;
+ });
}
@computed get noAutoLink() {
@@ -695,7 +697,6 @@ interface RichTextMenuPluginProps {
editorProps: FormattedTextBoxProps;
}
export class RichTextMenuPlugin extends React.Component<RichTextMenuPluginProps> {
- // eslint-disable-next-line react/no-unused-class-component-methods
update(view: EditorView & { TextView?: FormattedTextBox }, lastState: EditorState | undefined) {
RichTextMenu.Instance?.updateMenu(view, lastState, this.props.editorProps, view.TextView?.layoutDoc);
}
diff --git a/src/client/views/nodes/generativeFill/GenerativeFill.tsx b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
index 6d8ba9222..261eb4bb4 100644
--- a/src/client/views/nodes/generativeFill/GenerativeFill.tsx
+++ b/src/client/views/nodes/generativeFill/GenerativeFill.tsx
@@ -21,19 +21,16 @@ import { CollectionFreeFormView } from '../../collections/collectionFreeForm';
import { ImageEditorData } from '../ImageBox';
import { OpenWhereMod } from '../OpenWhere';
import './GenerativeFill.scss';
-import Buttons from './GenerativeFillButtons';
-import { BrushHandler } from './generativeFillUtils/BrushHandler';
+import { EditButtons, CutButtons } from './GenerativeFillButtons';
+import { BrushHandler, BrushType } from './generativeFillUtils/BrushHandler';
import { APISuccess, ImageUtility } from './generativeFillUtils/ImageHandler';
import { PointerHandler } from './generativeFillUtils/PointerHandler';
import { activeColor, canvasSize, eraserColor, freeformRenderSize, newCollectionSize, offsetDistanceY, offsetX } from './generativeFillUtils/generativeFillConstants';
import { CursorData, ImageDimensions, Point } from './generativeFillUtils/generativeFillInterfaces';
import { DocumentView } from '../DocumentView';
-
-// enum BrushStyle {
-// ADD,
-// SUBTRACT,
-// MARQUEE,
-// }
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { ImageField } from '../../../../fields/URLField';
+import { resolve } from 'url';
interface GenerativeFillProps {
imageEditorOpen: boolean;
@@ -82,6 +79,9 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
const parentDoc = useRef<Doc | null>(null);
const childrenDocs = useRef<Doc[]>([]);
+ // constants for image cutting
+ const cutPts = useRef<Point[]>([]);
+
// Undo and Redo
const handleUndo = () => {
const ctx = ImageUtility.getCanvasContext(canvasRef);
@@ -161,7 +161,8 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
x: currPoint.x - e.movementX / canvasScale,
y: currPoint.y - e.movementY / canvasScale,
};
- BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor /* , brushStyle === BrushStyle.SUBTRACT */);
+ const pts = BrushHandler.createBrushPathOverlay(lastPoint, currPoint, cursorData.width / 2 / canvasScale, ctx, eraserColor, BrushType.CUT);
+ cutPts.current.push(...pts);
};
drawingAreaRef.current?.addEventListener('pointermove', handlePointerMove);
@@ -278,7 +279,6 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
const maskBlob = await ImageUtility.canvasToBlob(canvasMask);
const imgBlob = await ImageUtility.canvasToBlob(canvasOriginalImg);
const res = await ImageUtility.getEdit(imgBlob, maskBlob, input !== '' ? input + ' in the same style' : 'Fill in the image in the same style', 2);
- // const res = await ImageUtility.mockGetEdit(img.src);
// create first image
if (!newCollectionRef.current) {
@@ -334,6 +334,68 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
setLoading(false);
};
+ const cutImage = async () => {
+ const img = currImg.current;
+ const canvas = canvasRef.current;
+ if (!canvas || !img) return;
+ canvas.width = img.naturalWidth;
+ canvas.height = img.naturalHeight;
+ const ctx = ImageUtility.getCanvasContext(canvasRef);
+ if (!ctx) return;
+ ctx.globalCompositeOperation = 'source-over';
+ setLoading(true);
+ setEdited(true);
+ // get the original image
+ const canvasOriginalImg = ImageUtility.getCanvasImg(img);
+ if (!canvasOriginalImg) return;
+ // draw the image onto the canvas
+ ctx.drawImage(img, 0, 0);
+ // get the mask which i assume is the thing the user draws on
+ // const canvasMask = ImageUtility.getCanvasMask(canvas, canvasOriginalImg);
+ // if (!canvasMask) return;
+ // canvasMask.width = canvas.width;
+ // canvasMask.height = canvas.height;
+ // now put the user's path around the mask
+ if (cutPts.current.length) {
+ ctx.beginPath();
+ ctx.moveTo(cutPts.current[0].x, cutPts.current[0].y); // later check edge case where cutPts is empty
+ for (let i = 0; i < cutPts.current.length; i++) {
+ ctx.lineTo(cutPts.current[i].x, cutPts.current[i].y);
+ }
+ ctx.closePath();
+ ctx.stroke();
+ ctx.fill();
+ // ctx.clip();
+ }
+ const url = canvas.toDataURL(); // this does the same thing as convert img to canvasurl
+ if (!newCollectionRef.current) {
+ if (!isNewCollection && imageRootDoc) {
+ // if the parent hasn't been set yet
+ if (!parentDoc.current) parentDoc.current = imageRootDoc;
+ } else {
+ if (!(originalImg.current && imageRootDoc)) return;
+ // create new collection and add it to the view
+ newCollectionRef.current = Docs.Create.FreeformDocument([], {
+ x: NumCast(imageRootDoc.x) + NumCast(imageRootDoc._width) + offsetX,
+ y: NumCast(imageRootDoc.y),
+ _width: newCollectionSize,
+ _height: newCollectionSize,
+ title: 'Image edit collection',
+ });
+ DocUtils.MakeLink(imageRootDoc, newCollectionRef.current, { link_relationship: 'Image Edit Version History' });
+ // opening new tab
+ CollectionDockingView.AddSplit(newCollectionRef.current, OpenWhereMod.right);
+ }
+ }
+ const image = new Image();
+ image.src = url;
+ await createNewImgDoc(image, true);
+ // add the doc to the main freeform
+ // eslint-disable-next-line no-use-before-define
+ setLoading(false);
+ cutPts.current.length = 0;
+ };
+
// adjusts all the img positions to be aligned
const adjustImgPositions = () => {
if (!parentDoc.current) return;
@@ -439,6 +501,7 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
<div className="generativeFillContainer" style={{ display: imageEditorOpen ? 'flex' : 'none' }}>
<div className="generativeFillControls">
<h1>Image Editor</h1>
+ {/* <IconButton text="Cut out" icon={<FontAwesomeIcon icon="scissors" />} /> */}
<div style={{ display: 'flex', alignItems: 'center', gap: '1.5rem' }}>
<FormControlLabel
control={
@@ -455,7 +518,8 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
labelPlacement="end"
sx={{ whiteSpace: 'nowrap' }}
/>
- <Buttons getEdit={getEdit} loading={loading} onReset={handleReset} />
+ <EditButtons onClick={getEdit} loading={loading} onReset={handleReset} />
+ <CutButtons onClick={cutImage} loading={loading} onReset={handleReset} />
<IconButton color={activeColor} tooltip="close" icon={<CgClose size="16px" />} onClick={handleViewClose} />
</div>
</div>
@@ -526,6 +590,24 @@ const GenerativeFill = ({ imageEditorOpen, imageEditorSource, imageRootDoc, addD
}}
/>
</div>
+ <div onPointerDown={e => e.stopPropagation()} style={{ height: 225, width: '100%', display: 'flex', justifyContent: 'center', cursor: 'pointer' }}>
+ <Slider
+ sx={{
+ '& input[type="range"]': {
+ WebkitAppearance: 'slider-vertical',
+ },
+ }}
+ orientation="vertical"
+ min={1}
+ max={500}
+ defaultValue={150}
+ size="small"
+ valueLabelDisplay="auto"
+ onChange={(e: any, val: any) => {
+ setCursorData(prev => ({ ...prev, width: val as number }));
+ }}
+ />
+ </div>
</div>
{/* Edits thumbnails */}
<div className="editsBox">
diff --git a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx
index d1f68ee0e..fe22b273d 100644
--- a/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx
+++ b/src/client/views/nodes/generativeFill/GenerativeFillButtons.tsx
@@ -6,12 +6,12 @@ import { AiOutlineInfo } from 'react-icons/ai';
import { activeColor } from './generativeFillUtils/generativeFillConstants';
interface ButtonContainerProps {
- getEdit: () => Promise<void>;
+ onClick: () => Promise<void>;
loading: boolean;
onReset: () => void;
}
-function Buttons({ loading, getEdit, onReset }: ButtonContainerProps) {
+export function EditButtons({ loading, onClick: getEdit, onReset }: ButtonContainerProps) {
return (
<div className="generativeFillBtnContainer">
<Button text="RESET" type={Type.PRIM} color={activeColor} onClick={onReset} />
@@ -41,4 +41,32 @@ function Buttons({ loading, getEdit, onReset }: ButtonContainerProps) {
);
}
-export default Buttons;
+export function CutButtons({ loading, onClick: cutImage, onReset }: ButtonContainerProps) {
+ return (
+ <div className="generativeFillBtnContainer">
+ <Button text="RESET" type={Type.PRIM} color={activeColor} onClick={onReset} />
+ {loading ? (
+ <Button
+ text="CUT IMAGE"
+ type={Type.TERT}
+ color={activeColor}
+ icon={<ReactLoading type="spin" color="#ffffff" width={20} height={20} />}
+ iconPlacement="right"
+ onClick={() => {
+ if (!loading) cutImage();
+ }}
+ />
+ ) : (
+ <Button
+ text="CUT IMAGE"
+ type={Type.TERT}
+ color={activeColor}
+ onClick={() => {
+ if (!loading) cutImage();
+ }}
+ />
+ )}
+ <IconButton type={Type.SEC} color={activeColor} tooltip="Open Documentation" icon={<AiOutlineInfo size="16px" />} onClick={() => window.open('https://brown-dash.github.io/Dash-Documentation/features/generativeai/#editing', '_blank')} />
+ </div>
+ );
+}
diff --git a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts
index 16d529d93..8a66d7347 100644
--- a/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts
+++ b/src/client/views/nodes/generativeFill/generativeFillUtils/BrushHandler.ts
@@ -1,6 +1,12 @@
import { GenerativeFillMathHelpers } from './GenerativeFillMathHelpers';
import { eraserColor } from './generativeFillConstants';
import { Point } from './generativeFillInterfaces';
+import { points } from '@turf/turf';
+
+export enum BrushType {
+ GEN_FILL,
+ CUT,
+}
export class BrushHandler {
static brushCircleOverlay = (x: number, y: number, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string /* , erase: boolean */) => {
@@ -14,12 +20,16 @@ export class BrushHandler {
ctx.closePath();
};
- static createBrushPathOverlay = (startPoint: Point, endPoint: Point, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string /* , erase: boolean */) => {
+ static createBrushPathOverlay = (startPoint: Point, endPoint: Point, brushRadius: number, ctx: CanvasRenderingContext2D, fillColor: string, brushType: BrushType) => {
const dist = GenerativeFillMathHelpers.distanceBetween(startPoint, endPoint);
-
+ const pts: Point[] = [];
for (let i = 0; i < dist; i += 5) {
const s = i / dist;
- BrushHandler.brushCircleOverlay(startPoint.x * (1 - s) + endPoint.x * s, startPoint.y * (1 - s) + endPoint.y * s, brushRadius, ctx, fillColor /* , erase */);
+ const x = startPoint.x * (1 - s) + endPoint.x * s;
+ const y = startPoint.y * (1 - s) + endPoint.y * s;
+ pts.push({ x: startPoint.x, y: startPoint.y });
+ BrushHandler.brushCircleOverlay(x, y, brushRadius, ctx, fillColor);
}
+ return pts;
};
}
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 6dd036cf6..bff112017 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -1,21 +1,24 @@
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { ColorPicker, Group, IconButton, Popup, Size, Toggle, ToggleType, Type } from 'browndash-components';
-import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction } from 'mobx';
+import { IReactionDisposer, ObservableMap, action, computed, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
import { ColorResult } from 'react-color';
+import ReactLoading from 'react-loading';
import { ClientUtils, returnFalse, setupMoveUpEvents } from '../../../ClientUtils';
import { emptyFunction, unimplementedFunction } from '../../../Utils';
import { Doc, Opt } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
import { GPTCallType, gptAPICall } from '../../apis/gpt/GPT';
import { Docs } from '../../documents/Documents';
import { SettingsManager } from '../../util/SettingsManager';
+import { undoBatch } from '../../util/UndoManager';
import { AntimodeMenu, AntimodeMenuProps } from '../AntimodeMenu';
import { LinkPopup } from '../linking/LinkPopup';
import { DocumentView } from '../nodes/DocumentView';
+import { DrawingOptions, SmartDrawHandler } from '../smartdraw/SmartDrawHandler';
import './AnchorMenu.scss';
import { GPTPopup, GPTPopupMode } from './GPTPopup/GPTPopup';
-import ReactLoading from 'react-loading';
@observer
export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@@ -44,6 +47,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
@observable private _selectedText: string = '';
@observable private _x: number = 0;
@observable private _y: number = 0;
+ @observable private _isLoading: boolean = false;
@action
public setSelectedText = (txt: string) => {
this._selectedText = txt.trim();
@@ -78,6 +82,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
public get Active() {
return this._left > 0;
}
+ public AddDrawingAnnotation: (doc: Doc) => void = unimplementedFunction;
public addToCollection: ((doc: Doc | Doc[], annotationKey?: string | undefined) => boolean) | undefined;
componentWillUnmount() {
@@ -137,10 +142,9 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
transferToFlashcard = (text: string, x: number, y: number) => {
// put each question generated by GPT on the front of the flashcard
- var senArr = text.trim().split('Question: ');
- var collectionArr: Doc[] = [];
+ const senArr = text.trim().split('Question: ');
+ const collectionArr: Doc[] = [];
for (let i = 1; i < senArr.length; i++) {
- console.log('Arr ' + i + ': ' + senArr[i]);
const newDoc = Docs.Create.ComparisonDocument(senArr[i], { _layout_isFlashcard: true, _width: 300, _height: 300 });
newDoc.text = senArr[i];
@@ -164,6 +168,35 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
this._loading = false;
};
+ /**
+ * Creates a GPT drawing based on selected text.
+ */
+ gptDraw = async (e: React.PointerEvent) => {
+ try {
+ SmartDrawHandler.Instance.AddDrawing = this.createDrawingAnnotation;
+ runInAction(() => (this._isLoading = true));
+ await SmartDrawHandler.Instance.drawWithGPT({ X: e.clientX, Y: e.clientY }, this._selectedText, 5, 100, true);
+ runInAction(() => (this._isLoading = false));
+ } catch (err) {
+ console.error(err);
+ }
+ };
+
+ /**
+ * Defines how a GPT drawing should be added to the current document.
+ */
+ @undoBatch
+ createDrawingAnnotation = action((drawing: Doc, opts: DrawingOptions, gptRes: string) => {
+ this.AddDrawingAnnotation(drawing);
+ const docData = drawing[DocData];
+ docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
+ docData.drawingInput = opts.text;
+ docData.drawingComplexity = opts.complexity;
+ docData.drawingColored = opts.autoColor;
+ docData.drawingSize = opts.size;
+ docData.drawingData = gptRes;
+ });
+
pointerDown = (e: React.PointerEvent) => {
setupMoveUpEvents(
this,
@@ -249,6 +282,14 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
{/* Adds a create flashcards option to the anchor menu, which calls the gptFlashcard method. */}
<IconButton tooltip="Create flashcards" onPointerDown={this.gptFlashcards} icon={<FontAwesomeIcon icon="id-card" size="lg" />} color={SettingsManager.userColor} />
<IconButton tooltip="Create labels" onPointerDown={this.makeLabels} icon={<FontAwesomeIcon icon="tag" size="lg" />} color={SettingsManager.userColor} />
+ {this._selectedText && (
+ <IconButton
+ tooltip="Create drawing"
+ onPointerDown={e => this.gptDraw(e)}
+ icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon="paintbrush" size="lg" />}
+ color={SettingsManager.userColor}
+ />
+ )}
{AnchorMenu.Instance.OnAudio === unimplementedFunction ? null : (
<IconButton
tooltip="Click to Record Annotation" //
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index 1d7f269fb..fc74a480e 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -544,6 +544,14 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
// allows for creating collection
AnchorMenu.Instance.addToCollection = this._props.DocumentView?.()._props.addDocument;
AnchorMenu.Instance.gptFlashcards = this.gptPDFFlashcards;
+ AnchorMenu.Instance.AddDrawingAnnotation = this.addDrawingAnnotation;
+ };
+
+ addDrawingAnnotation = (drawing: Doc) => {
+ // drawing[DocData].x = this._props.pdfBox.ScreenToLocalBoxXf().TranslateX
+ // const scaleX = this._mainCont.current.offsetWidth / boundingRect.width;
+ drawing.y = NumCast(drawing.y) + NumCast(this._props.Document.layout_scrollTop);
+ this._props.addDocument?.(drawing);
};
@action
@@ -617,7 +625,6 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
return (
<div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this._props.Document), transform: `scale(${NumCast(this._props.layoutDoc._freeform_scale, 1)})` }} ref={this._annotationLayer}>
{inlineAnnos.map(anno => (
- // eslint-disable-next-line react/jsx-props-no-spreading
<Annotation {...this._props} fieldKey={this._props.fieldKey + '_annotations'} pointerEvents={this.pointerEvents} containerDataDoc={this._props.dataDoc} annoDoc={anno} key={`${anno[Id]}-annotation`} />
))}
</div>
@@ -636,7 +643,6 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
if (this.inlineTextAnnotations.includes(doc) || this._props.isContentActive() === false) return 'none';
const isInk = doc.layout_isSvg && !props?.LayoutTemplateString;
if (isInk) return 'visiblePainted';
- //return isInk ? 'visiblePainted' : 'all';
}
return this._props.styleProvider?.(doc, props, property);
};
@@ -651,7 +657,6 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined,
}}>
<CollectionFreeFormView
- // eslint-disable-next-line react/jsx-props-no-spreading
{...this._props}
NativeWidth={returnZero}
NativeHeight={returnZero}
diff --git a/src/client/views/smartdraw/AnnotationPalette.scss b/src/client/views/smartdraw/AnnotationPalette.scss
new file mode 100644
index 000000000..4f11e8afc
--- /dev/null
+++ b/src/client/views/smartdraw/AnnotationPalette.scss
@@ -0,0 +1,56 @@
+.annotation-palette {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ position: absolute;
+ right: 14px;
+ top: 50px;
+ border-radius: 5px;
+ margin: auto;
+}
+
+.palette-create {
+ display: flex;
+ flex-direction: row;
+ width: 170px;
+
+ .palette-create-input {
+ color: black;
+ width: 170px;
+ }
+}
+
+.palette-create-options {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ width: 170px;
+ margin-top: 5px;
+
+ .palette-color {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 40px;
+ }
+
+ .palette-detail,
+ .palette-size {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 60px;
+ }
+}
+
+.palette-buttons {
+ width: 100%;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+}
+
+.palette-save-reset {
+ display: flex;
+ flex-direction: row;
+}
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
new file mode 100644
index 000000000..f1e2e4f41
--- /dev/null
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -0,0 +1,361 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Slider, Switch } from '@mui/material';
+import { Button } from 'browndash-components';
+import { action, 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 { returnEmptyFilter, returnFalse, returnTrue } from '../../../ClientUtils';
+import { emptyFunction } from '../../../Utils';
+import { Doc, DocListCast, returnEmptyDoclist } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
+import { ImageCast, NumCast } from '../../../fields/Types';
+import { ImageField } from '../../../fields/URLField';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { Docs } from '../../documents/Documents';
+import { makeUserTemplateButtonOrImage } from '../../util/DropConverter';
+import { SettingsManager } from '../../util/SettingsManager';
+import { Transform } from '../../util/Transform';
+import { undoBatch } from '../../util/UndoManager';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { DefaultStyleProvider, returnEmptyDocViewList } from '../StyleProvider';
+import { DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
+import { FieldView } from '../nodes/FieldView';
+import './AnnotationPalette.scss';
+import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
+
+interface AnnotationPaletteProps {
+ Document: Doc;
+}
+
+/**
+ * The AnnotationPalette can be toggled in the lightbox view of a document. The goal of the palette
+ * is to offer an easy way for users to save then drag and drop repeated annotations onto a document.
+ * These annotations can be of any annotation type and operate similarly to user templates.
+ *
+ * On the "add" side of the palette, there is a way to create a drawing annotation with GPT. Users can
+ * enter the item to draw, toggle different settings, then GPT will generate three versions of the drawing
+ * to choose from. These drawings can then be saved to the palette as annotations.
+ */
+@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: AnnotationPaletteProps) {
+ 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.generateDrawings();
+ }
+ };
+
+ @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;
+ };
+
+ /**
+ * Adds a doc to the annotation palette. Gets a snapshot of the document to use as a preview in the palette. When this
+ * preview is dragged onto a parent document, a copy of that document is added as an annotation.
+ */
+ public static addToPalette = async (doc: Doc) => {
+ if (!doc.savedAsAnno) {
+ const docView = DocumentView.getDocumentView(doc);
+ await docView?.ComponentView?.updateIcon?.(true);
+ const { clone } = await Doc.MakeClone(doc);
+ clone.title = doc.title;
+ const image = ImageCast(doc.icon, ImageCast(clone[Doc.LayoutFieldKey(clone)]))?.url?.href;
+ Doc.AddDocToList(Doc.MyAnnos, 'data', makeUserTemplateButtonOrImage(clone, image));
+ 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;
+ }
+
+ /**
+ * Calls the draw with GPT functions in SmartDrawHandler to allow users to generate drawings straight from
+ * the annotation palette.
+ */
+ @undoBatch
+ generateDrawings = action(async () => {
+ this._isLoading = true;
+ this._props.Document[DocData].data = undefined;
+ for (let i = 0; i < 3; i++) {
+ try {
+ SmartDrawHandler.Instance.AddDrawing = this.addDrawing;
+ this._canInteract = false;
+ if (this._showRegenerate) {
+ 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', e);
+ }
+ }
+ 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
+ addDrawing = (drawing: Doc, opts: DrawingOptions, gptRes: string) => {
+ this._gptRes.push(gptRes);
+ drawing[DocData].freeform_fitContentsToBox = true;
+ Doc.AddDocToList(this._props.Document, 'data', drawing);
+ };
+
+ /**
+ * Saves the currently showing, newly generated drawing to the annotation palette and sets the metadata.
+ * AddToPalette() is generically used to add any document to the palette, while this defines the behavior for when a user
+ * presses the "save drawing" button.
+ */
+ saveDrawing = async () => {
+ const cIndex = NumCast(this._props.Document.carousel_index);
+ 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;
+ docData.x = this._opts.x;
+ docData.y = this._opts.y;
+ await AnnotationPalette.addToPalette(focusedDrawing);
+ this.resetPalette(true);
+ };
+
+ 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={returnEmptyDocViewList}
+ 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 className="palette-create">
+ <input
+ className="palette-create-input"
+ aria-label="label-input"
+ id="new-label"
+ type="text"
+ 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.generateDrawings}
+ />
+ </div>
+ <div className="palette-create-options">
+ <div className="palette-color">
+ 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 className="palette-detail">
+ 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 className="palette-size">
+ 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={returnEmptyDocViewList}
+ 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 className="palette-buttons">
+ <Button text="Back" tooltip="Back to All Annotations" icon={<FontAwesomeIcon icon="reply" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(true)} />
+ <div className="palette-save-reset">
+ <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..0e8bd3349
--- /dev/null
+++ b/src/client/views/smartdraw/SmartDrawHandler.scss
@@ -0,0 +1,44 @@
+.smart-draw-handler {
+ position: absolute;
+}
+
+.smartdraw-input {
+ color: black;
+}
+
+.smartdraw-options {
+ display: flex;
+ flex-direction: row;
+ justify-content: space-around;
+
+ .auto-color {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ width: 30%;
+ }
+
+ .complexity {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ width: 31%;
+ }
+
+ .size {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ width: 39%;
+
+ .size-slider {
+ width: 80%;
+ }
+ }
+}
+
+.regenerate-box,
+.edit-box {
+ display: flex;
+ flex-direction: row;
+}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
new file mode 100644
index 000000000..75ef55060
--- /dev/null
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -0,0 +1,491 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Slider, Switch } from '@mui/material';
+import { Button, IconButton } from 'browndash-components';
+import { action, makeObservable, observable, runInAction } from 'mobx';
+import { observer } from 'mobx-react';
+import React from 'react';
+import { AiOutlineSend } from 'react-icons/ai';
+import ReactLoading from 'react-loading';
+import { INode, parse } from 'svgson';
+import { unimplementedFunction } from '../../../Utils';
+import { Doc, DocListCast } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
+import { InkData, InkField, InkTool } from '../../../fields/InkField';
+import { BoolCast, ImageCast, NumCast, StrCast } from '../../../fields/Types';
+import { GPTCallType, gptAPICall, gptDrawingColor } from '../../apis/gpt/GPT';
+import { Docs } from '../../documents/Documents';
+import { SettingsManager } from '../../util/SettingsManager';
+import { undoable } from '../../util/UndoManager';
+import { SVGToBezier, SVGType } from '../../util/bezierFit';
+import { InkingStroke } from '../InkingStroke';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { CollectionCardView } from '../collections/CollectionCardDeckView';
+import { MarqueeView } from '../collections/collectionFreeForm';
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, ActiveIsInkMask, DocumentView } from '../nodes/DocumentView';
+import './SmartDrawHandler.scss';
+
+export interface DrawingOptions {
+ text: string;
+ complexity: number;
+ size: number;
+ autoColor: boolean;
+ x: number;
+ y: number;
+}
+
+/**
+ * The SmartDrawHandler allows users to generate drawings with GPT from text input. Users are able to enter
+ * the item to draw, how complex they want the drawing to be, how large the drawing should be, and whether
+ * it will be colored. If the drawing is colored, GPT will automatically define the stroke and fill of each
+ * stroke. Drawings are retrieved from GPT as SVG code then converted into Dash-supported Beziers.
+ *
+ * The handler is selected from the ink tools menu. To generate a drawing, users can click anywhere on the freeform
+ * canvas and a popup will appear that prompts them to create a drawing. Once the drawing is created, users have
+ * the option to regenerate or edit the drawing.
+ *
+ * When each drawing is created, it is added to Dash as a group of ink strokes. The group is tagged with metadata
+ * for user input, the drawing's SVG code, and its settings (size, complexity). In the context menu -> 'Options',
+ * users can then show the drawing editor and regenerate/edit them at any point in the future.
+ */
+
+@observer
+export class SmartDrawHandler extends ObservableReactComponent<object> {
+ // eslint-disable-next-line no-use-before-define
+ static Instance: SmartDrawHandler;
+
+ 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;
+
+ @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 private _complexity: number = 5;
+ @observable private _size: number = 200;
+ @observable private _autoColor: boolean = true;
+ @observable private _regenInput: string = '';
+ @observable private _canInteract: boolean = true;
+
+ @observable public ShowRegenerate: boolean = false;
+
+ constructor(props: object) {
+ super(props);
+ makeObservable(this);
+ SmartDrawHandler.Instance = this;
+ }
+
+ /**
+ * AddDrawing and RemoveDrawing are defined by the other classes that call the smart draw functions (i.e.
+ CollectionFreeForm, FormattedTextBox, AnnotationPalette) to define how a drawing document should be added
+ or removed in their respective locations (to the freeform canvs, to the annotation palette's preview, etc.)
+ */
+ public AddDrawing: (doc: Doc, opts: DrawingOptions, gptRes: string) => void = unimplementedFunction;
+ public RemoveDrawing: (useLastContainer: boolean, doc?: Doc) => void = unimplementedFunction;
+ /**
+ * This creates the ink document that represents a drawing, so it goes through the strokes that make up the drawing,
+ * creates ink documents for each stroke, then adds the strokes to a collection. This can also be redefined by other
+ * classes to customize the way the drawing docs get created. For example, the freeform canvas has a different way of
+ * defining document bounds, so CreateDrawingDoc is redefined when that class calls gpt draw functions.
+ */
+ public CreateDrawingDoc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => Doc | undefined = (strokeList: [InkData, string, string][], opts: DrawingOptions) => {
+ 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);
+ });
+
+ return MarqueeView.getCollection(drawing, undefined, true, { left: 1, top: 1, width: 1, height: 1 });
+ };
+
+ @action
+ displaySmartDrawHandler = (x: number, y: number) => {
+ [this._pageX, this._pageY] = [x, y];
+ this._display = true;
+ };
+
+ /**
+ * This is called in two places: 1. In this class, where the regenerate popup shows as soon as a
+ * drawing is created to replace the original smart draw popup. 2. From the context menu to make
+ * the regenerate popup show by user command.
+ */
+ @action
+ displayRegenerate = (x: number, y: number) => {
+ this._selectedDoc = DocumentView.SelectedDocs()?.lastElement();
+ [this._pageX, this._pageY] = [x, y];
+ this._display = false;
+ this.ShowRegenerate = true;
+ this._showEditBox = false;
+ const docData = this._selectedDoc[DocData];
+ this._lastResponse = StrCast(docData.drawingData);
+ this._lastInput = { text: StrCast(docData.drawingInput), complexity: NumCast(docData.drawingComplexity), size: NumCast(docData.drawingSize), autoColor: BoolCast(docData.drawingColored), x: this._pageX, y: this._pageY };
+ };
+
+ /**
+ * Hides the smart draw handler and resets its fields to their default.
+ */
+ @action
+ hideSmartDrawHandler = () => {
+ if (this._display) {
+ 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;
+ }
+ };
+
+ /**
+ * Hides the popup that allows users to regenerate a drawing and resets its corresponding fields.
+ */
+ @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 };
+ }
+ };
+
+ /**
+ * This allows users to press the return/enter key to send input.
+ */
+ handleKeyPress = (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ this.handleSendClick();
+ }
+ };
+
+ /**
+ * This is called when a user hits "send" on the draw with GPT popup. It calls the drawWithGPT or regenerate
+ * functions depending on what mode is currently displayed, then sets various observable fields that facilitate
+ * what the user sees.
+ */
+ @action
+ handleSendClick = async () => {
+ this._isLoading = true;
+ this._canInteract = false;
+ if (this.ShowRegenerate) {
+ await this.regenerate();
+ runInAction(() => {
+ this._regenInput = '';
+ this._showEditBox = false;
+ });
+ } else {
+ runInAction(() => {
+ this._showOptions = false;
+ });
+ try {
+ await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+ this.hideSmartDrawHandler();
+
+ runInAction(() => {
+ this.ShowRegenerate = true;
+ });
+ } 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);
+ }
+ }
+ }
+ runInAction(() => {
+ this._isLoading = false;
+ this._canInteract = true;
+ });
+ };
+
+ /**
+ * Calls GPT API to create a drawing based on user input.
+ */
+ @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;
+ }
+ const strokeData = await this.parseSvg(res, startPt, false, autoColor);
+ const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
+ drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+
+ this._errorOccurredOnce = false;
+ return strokeData;
+ };
+
+ /**
+ * Regenerates drawings with the option to add a specific regenerate prompt/request.
+ */
+ @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;
+ }
+ const strokeData = await this.parseSvg(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+ this.RemoveDrawing !== unimplementedFunction && this.RemoveDrawing(true, this._selectedDoc);
+ const drawingDoc = strokeData && this.CreateDrawingDoc(strokeData.data, strokeData.lastInput, strokeData.lastRes);
+ drawingDoc && this.AddDrawing(drawingDoc, this._lastInput, res);
+ return strokeData;
+ } catch (err) {
+ console.error('Error regenerating drawing', err);
+ }
+ };
+
+ /**
+ * Parses the svg code that GPT returns into Bezier curves, with coordinates and colors.
+ */
+ @action
+ parseSvg = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => {
+ const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
+ if (svg) {
+ this._lastResponse = svg[0];
+ const svgObject = await parse(svg[0]);
+ const svgStrokes: INode[] = svgObject.children;
+ const strokeData: [InkData, string, string][] = [];
+ svgStrokes.forEach(child => {
+ const convertedBezier: InkData = SVGToBezier(child.name as SVGType, child.attributes);
+ strokeData.push([
+ convertedBezier.map(point => ({ 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 : '',
+ (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : '',
+ ]);
+ });
+ return { data: strokeData, lastInput: this._lastInput, lastRes: svg[0] };
+ }
+ };
+
+ /**
+ * Sends request to GPT API to recolor a selected ink document or group of ink documents.
+ */
+ colorWithGPT = async (drawing: Doc) => {
+ const img = await DocumentView.GetDocImage(drawing);
+ const { href } = ImageCast(img).url;
+ const hrefParts = href.split('.');
+ const hrefComplete = `${hrefParts[0]}_o.${hrefParts[1]}`;
+ try {
+ const hrefBase64 = await CollectionCardView.imageUrlToBase64(hrefComplete);
+ const strokes = DocListCast(drawing[DocData].data);
+ const coords: string[] = [];
+ strokes.forEach((stroke, i) => {
+ const inkingStroke = DocumentView.getDocumentView(stroke)?.ComponentView as InkingStroke;
+ const { inkData } = inkingStroke.inkScaledData();
+ coords.push(`${i + 1}. ${inkData.filter((point, index) => index % 4 === 0 || index == inkData.length - 1).map(point => `(${point.X.toString()}, ${point.Y.toString()})`)}`);
+ });
+ const colorResponse = await gptDrawingColor(hrefBase64, coords).then(response => gptAPICall(response, GPTCallType.COLOR, undefined));
+ this.colorStrokes(colorResponse, drawing);
+ } catch (error) {
+ console.log('GPT call failed', error);
+ }
+ };
+
+ /**
+ * Function that parses the GPT color response and sets the selected stroke(s) to the new color.
+ */
+ colorStrokes = undoable((res: string, drawing: Doc) => {
+ const colorList = res.match(/\{.*?\}/g);
+ const strokes = DocListCast(drawing[DocData].data);
+ colorList?.forEach((colors, index) => {
+ const strokeAndFill = colors.match(/#[0-9A-Fa-f]{6}/g);
+ if (strokeAndFill && strokeAndFill.length == 2) {
+ strokes[index][DocData].color = strokeAndFill[0];
+ const inkStroke = DocumentView.getDocumentView(strokes[index])?.ComponentView as InkingStroke;
+ const { inkData } = inkStroke.inkScaledData();
+ InkingStroke.IsClosed(inkData) ? (strokes[index][DocData].fillColor = strokeAndFill[1]) : (strokes[index][DocData].fillColor = undefined);
+ }
+ });
+ }, 'color strokes');
+
+ renderDisplay() {
+ 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}
+ />
+ <input
+ aria-label="Smart Draw Input"
+ className="smartdraw-input"
+ type="text"
+ autoFocus
+ value={this._userInput}
+ onChange={action(e => this._canInteract && (this._userInput = 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} onClick={action(() => (this._showOptions = !this._showOptions))} />
+ <Button
+ style={{ alignSelf: 'flex-end' }}
+ text="Send"
+ icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ color={SettingsManager.userColor}
+ onClick={this.handleSendClick}
+ />
+ </div>
+ {this._showOptions && (
+ <div className="smartdraw-options">
+ <div className="auto-color">
+ 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={action(() => this._canInteract && (this._autoColor = !this._autoColor))}
+ />
+ </div>
+ <div className="complexity">
+ Complexity
+ <Slider
+ sx={{
+ '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+ '& .MuiSlider-rail': { color: SettingsManager.userColor },
+ '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10` } },
+ }}
+ style={{ width: '80%' }}
+ min={1}
+ max={10}
+ step={1}
+ size="small"
+ value={this._complexity}
+ onChange={action((e, val) => this._canInteract && (this._complexity = val as number))}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ <div className="size">
+ Size (in pixels)
+ <Slider
+ className="size-slider"
+ sx={{
+ '& .MuiSlider-track': { color: SettingsManager.userVariantColor },
+ '& .MuiSlider-rail': { color: SettingsManager.userColor },
+ '& .MuiSlider-thumb': { color: SettingsManager.userColor, '&.Mui-focusVisible, &:hover, &.Mui-active': { boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20` } },
+ }}
+ min={50}
+ max={700}
+ step={10}
+ size="small"
+ value={this._size}
+ onChange={action((e, val) => this._canInteract && (this._size = val as number))}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ </div>
+ )}
+ </div>
+ );
+ }
+
+ renderRegenerate() {
+ return (
+ <div
+ 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 className="regenerate-box">
+ <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={action(() => (this._showEditBox = !this._showEditBox))} />
+ {this._showEditBox && (
+ <div className="edit-box">
+ <input
+ aria-label="Edit instructions input"
+ className="smartdraw-input"
+ type="text"
+ value={this._regenInput}
+ onChange={action(e => this._canInteract && (this._regenInput = 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>
+ );
+ }
+
+ render() {
+ return this._display ? this.renderDisplay() : this.ShowRegenerate ? this.renderRegenerate() : null;
+ }
+}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 0f1609711..7a1ab3b4f 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -24,7 +24,6 @@ import { listSpec } from './Schema';
import { ComputedField, ScriptField } from './ScriptField';
import { BoolCast, Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor, toList } from './Types';
import { containedFieldChangedHandler, deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, setter, SharingPermissions } from './util';
-import { KEY } from 'google-maps';
export let ObjGetRefField: (id: string, force?: boolean) => Promise<Doc | undefined>;
export let ObjGetRefFields: (ids: string[]) => Promise<Map<string, Doc | undefined>>;
@@ -238,6 +237,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
@@ -336,7 +337,6 @@ export class Doc extends RefField {
if (!id || forceSave) {
DocServer.CreateDocField(docProxy);
}
- // eslint-disable-next-line no-constructor-return
return docProxy; // need to return the proxy from the constructor so that all our added fields will get called
}
@@ -464,7 +464,6 @@ export class Doc extends RefField {
}
}
-// eslint-disable-next-line no-redeclare
export namespace Doc {
export let SelectOnLoad: Doc | undefined;
export function SetSelectOnLoad(doc: Doc | undefined) {
@@ -660,7 +659,6 @@ export namespace Doc {
if (reversed) list.splice(0, 0, doc);
else list.push(doc);
} else {
- // eslint-disable-next-line no-lonely-if
if (reversed) list.splice(before ? list.length - ind + 1 : list.length - ind, 0, doc);
else list.splice(before ? ind : ind + 1, 0, doc);
}
@@ -1192,7 +1190,6 @@ export namespace Doc {
return Cast(Doc.UserDoc().myLinkDatabase, Doc, null);
}
export function SetUserDoc(doc: Doc) {
- // eslint-disable-next-line no-return-assign
return (manager._user_doc = doc);
}
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index 32abf0076..17b99b033 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -14,9 +14,11 @@ export enum InkTool {
StrokeEraser = 'strokeeraser',
SegmentEraser = 'segmenteraser',
RadiusEraser = 'radiuseraser',
+ Eraser = 'eraser', // not a real tool, but a class of tools
Stamp = 'stamp',
Write = 'write',
PresentationPin = 'presentationpin',
+ SmartDraw = 'smartdraw',
}
export type Segment = Array<Bezier>;
@@ -102,6 +104,26 @@ export class InkField extends ObjectField {
const top = Math.min(...ys);
return { right, left, bottom, top, width: right - left, height: bottom - top };
}
+
+ // for some reason bezier.js doesn't handle the case of intersecting a linear curve, so we wrap the intersection
+ // call in a test for linearity
+ public static bintersects(curve: Bezier, otherCurve: Bezier) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if ((curve as any)._linear) {
+ // bezier.js doesn't intersect properly if the curve is actually a line -- so get intersect other curve against this line, then figure out the t coordinates of the intersection on this line
+ const intersections = otherCurve.lineIntersects({ p1: curve.points[0], p2: curve.points[3] });
+ if (intersections.length) {
+ const intPt = otherCurve.get(intersections[0]);
+ const intT = curve.project(intPt).t;
+ return intT ? [intT] : [];
+ }
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ if ((otherCurve as any)._linear) {
+ return curve.lineIntersects({ p1: otherCurve.points[0], p2: otherCurve.points[3] });
+ }
+ return curve.intersects(otherCurve);
+ }
}
ScriptingGlobals.add('InkField', InkField);
diff --git a/src/fields/SchemaHeaderField.ts b/src/fields/SchemaHeaderField.ts
index 0a8dd1d9e..5f4d59cf9 100644
--- a/src/fields/SchemaHeaderField.ts
+++ b/src/fields/SchemaHeaderField.ts
@@ -79,7 +79,6 @@ export class SchemaHeaderField extends ObjectField {
@serializable(primitive())
desc: boolean | undefined; // boolean determines sort order, undefined when no sort
- // eslint-disable-next-line default-param-last
constructor(heading: string = '', color: string = RandomPastel(), type?: ColumnType, width?: number, desc?: boolean, collapsed?: boolean) {
super();
diff --git a/src/pen-gestures/GestureTypes.ts b/src/pen-gestures/GestureTypes.ts
index d86562580..5a8e9bd97 100644
--- a/src/pen-gestures/GestureTypes.ts
+++ b/src/pen-gestures/GestureTypes.ts
@@ -1,12 +1,12 @@
export enum Gestures {
Line = 'line',
Stroke = 'stroke',
- Scribble = 'scribble',
Text = 'text',
Triangle = 'triangle',
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..04262b61f 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,26 @@ 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.RightAngle, useBoundedRotationInvariance, new Array([new Point(0, 0), new Point(0, 100), new Point(200, 100)])));
NumMultistrokes = this.Multistrokes.length; // NumMultistrokes flags the end of the non user-defined gstures strokes
//
// PREDEFINED STROKES
diff --git a/src/typings/index.d.ts b/src/typings/index.d.ts
index fea88cdcb..bee79a38d 100644
--- a/src/typings/index.d.ts
+++ b/src/typings/index.d.ts
@@ -12,7 +12,7 @@ declare module 'fit-curve';
declare module 'iink-js';
declare module 'pdfjs-dist/web/pdf_viewer';
declare module 'react-jsx-parser';
-// declare module 'type_decls.d';
+declare module 'type_decls.d';
declare module '@react-pdf/renderer' {
import * as React from 'react';