aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/client/apis/gpt/GPT.ts11
-rw-r--r--src/client/documents/DocumentTypes.ts1
-rw-r--r--src/client/util/CurrentUserUtils.ts33
-rw-r--r--src/client/util/bezierFit.ts145
-rw-r--r--src/client/views/DocumentButtonBar.tsx27
-rw-r--r--src/client/views/GestureOverlay.tsx28
-rw-r--r--src/client/views/GlobalKeyHandler.ts2
-rw-r--r--src/client/views/LightboxView.scss24
-rw-r--r--src/client/views/LightboxView.tsx23
-rw-r--r--src/client/views/MainView.tsx17
-rw-r--r--src/client/views/MarqueeAnnotator.tsx146
-rw-r--r--src/client/views/StyleProvider.tsx2
-rw-r--r--src/client/views/collections/CollectionMenu.scss2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx276
-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.tsx6
-rw-r--r--src/client/views/global/globalScripts.ts4
-rw-r--r--src/client/views/nodes/PDFBox.scss16
-rw-r--r--src/client/views/nodes/PDFBox.tsx37
-rw-r--r--src/client/views/pdf/AnchorMenu.tsx14
-rw-r--r--src/client/views/pdf/PDFViewer.tsx3
-rw-r--r--src/client/views/smartdraw/AnnotationPalette.scss10
-rw-r--r--src/client/views/smartdraw/AnnotationPalette.tsx483
-rw-r--r--src/client/views/smartdraw/SmartDrawHandler.scss3
-rw-r--r--src/client/views/smartdraw/SmartDrawHandler.tsx439
-rw-r--r--src/fields/Doc.ts52
-rw-r--r--src/fields/InkField.ts1
28 files changed, 1692 insertions, 130 deletions
diff --git a/src/client/apis/gpt/GPT.ts b/src/client/apis/gpt/GPT.ts
index 2081f4a5e..cbe2f0582 100644
--- a/src/client/apis/gpt/GPT.ts
+++ b/src/client/apis/gpt/GPT.ts
@@ -12,6 +12,7 @@ enum GPTCallType {
DESCRIBE = 'describe',
MERMAID = 'mermaid',
DATA = 'data',
+ DRAW = 'draw',
}
type GPTCallOpts = {
@@ -53,6 +54,12 @@ const callTypeMap: { [type: string]: GPTCallOpts } = {
temp: 0,
prompt: 'List unique differences between the content of the UserAnswer and Rubric. Before each difference, label it and provide any additional information the UserAnswer missed and explain it in second person without separating it into UserAnswer and Rubric content and additional information. If there are no differences, say correct',
},
+ draw: {
+ model: 'gpt-4o',
+ maxTokens: 1024,
+ temp: 0.5,
+ prompt: 'Given an item, a level of complexity from 1-10, and a size in pixels, generate a detailed and colored line drawing representation of it. Make sure every element has the stroke field filled out. More complex drawings will have much more detail and strokes. The drawing should be in SVG format with no additional text or comments. For path coordinates, make sure you format with a comma between numbers, like M100,200 C150,250 etc. The only supported commands are line, ellipse, circle, rect, and path with M, Q, C, and L so only use those.',
+ },
};
let lastCall = '';
@@ -63,10 +70,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].includes(callType) ? inputTextIn + '.' : inputTextIn;
const opts: GPTCallOpts = callTypeMap[callType];
- if (lastCall === inputText) return lastResp;
+ if (lastCall === inputText && dontCache !== true) return lastResp;
try {
lastCall = inputText;
diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts
index 8f95068db..03ae2efb7 100644
--- a/src/client/documents/DocumentTypes.ts
+++ b/src/client/documents/DocumentTypes.ts
@@ -26,6 +26,7 @@ export enum DocumentType {
FUNCPLOT = 'funcplot', // function plotter
MAP = 'map',
DATAVIZ = 'dataviz',
+ ANNOPALETTE = 'annopalette',
LOADING = 'loading',
SIMULATION = 'simulation', // physics simulation
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 8ece897f4..98a54d4d0 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -162,6 +162,33 @@ 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", _chromeHidden: true, hidden: false,
+ _dragOnlyWithinContainer: true, layout_hideContextMenu: true, isSystem: true, _forceActive: true,
+ _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true,
+ };
+ const reqdScripts = { dropConverter: "convertToButtons(dragData)" };
+ const savedAnnos = DocCast(doc[field]);
+ return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.MasonryDocument(items??[], opts), reqdOpts, DocListCast(savedAnnos?.data), reqdScripts);
+ }
+
+ // static setupNoteTemplates(doc: Doc, field="template_notes") {
+ // const tempNotes = DocCast(doc[field]);
+ // const reqdTempOpts:DocumentOptions[] = [
+ // { title: "Postit", backgroundColor: "yellow", icon: "sticky-note", _layout_showTitle: "title", layout_borderRounding: "5px"},
+ // { title: "Idea", backgroundColor: "pink", icon: "lightbulb" , _layout_showTitle: "title"},
+ // { title: "Topic", backgroundColor: "lightblue", icon: "book-open" , _layout_showTitle: "title"}];
+ // const reqdNoteList = [...reqdTempOpts.map(opts => {
+ // const reqdOpts = {...opts, isSystem:true, width:200, layout_autoHeight: true, layout_fitWidth: true};
+ // const noteTemp = tempNotes ? DocListCast(tempNotes.data).find(fdoc => fdoc.title === opts.title): undefined;
+ // return DocUtils.AssignOpts(noteTemp, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts));
+ // }), ... DocListCast(tempNotes?.data).filter(note => !reqdTempOpts.find(reqd => reqd.title === note.title))];
+
+ // const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true };
+ // // eslint-disable-next-line no-return-assign
+ // r
// setup templates for different document types when they are iconified from Document Decorations
static setupDefaultIconTemplates(doc: Doc, field="template_icons") {
@@ -414,6 +441,7 @@ pie title Minerals in my tap water
{ toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "person-chalkboard", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay, funcs: { hidden: "IsNoviceMode()"}},
{ toolTip: "Tap or drag to create a view slide", title: "View Slide", icon: "address-card", dragFactory: doc.emptyViewSlide as Doc,clickFactory: DocCast(doc.emptyViewSlide),openFactoryLocation: OpenWhere.overlay,funcs: { hidden: "IsNoviceMode()"}},
{ toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true, funcs: { hidden: "IsNoviceMode()"} },
+ { toolTip: "Tap or drag to create an annotation palette",title: "Annotation Palette", icon: "palette", dragFactory: doc.emptyAnnoPalette as Doc, clickFactory: DocCast(doc.emptyAnnoPalette)},
{ toolTip: "Toggle a Calculator REPL", title: "replviewer", icon: "calculator", clickFactory: '<ScriptingRepl />' as any, openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script
// { toolTip: "Toggle an UndoStack", title: "undostacker", icon: "calculator", clickFactory: "<UndoStack />" as any, openFactoryLocation: OpenWhere.overlay},
].map(tuple => (
@@ -730,6 +758,7 @@ 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: "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, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {toolType:"activeEraserTool()"},
subMenu: [
@@ -742,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()"}},
];
}
@@ -984,6 +1014,7 @@ 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.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/bezierFit.ts b/src/client/util/bezierFit.ts
index d6f3f2340..98a6feea0 100644
--- a/src/client/util/bezierFit.ts
+++ b/src/client/util/bezierFit.ts
@@ -2,8 +2,18 @@
/* eslint-disable prefer-destructuring */
/* eslint-disable no-param-reassign */
/* eslint-disable camelcase */
+import e from 'cors';
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 +567,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 +602,135 @@ export function FitOneCurve(d: Point[], tHat1?: Point, tHat2?: Point) {
return { finalCtrls, error };
}
+// alpha determines how far away the tangents are, or the "tightness" of the bezier
+export function GenerateControlPoints(coordinates: Point[], alpha = 0.1) {
+ const firstEnd = coordinates.length ? [coordinates[0], coordinates[0]] : [];
+ const lastEnd = coordinates.length ? [coordinates.lastElement(), coordinates.lastElement()] : [];
+ const points: Point[] = coordinates.slice(1, coordinates.length - 1).flatMap((pt, index, inkData) => {
+ const prevPt: Point = index === 0 ? firstEnd[0] : inkData[index - 1];
+ const nextPt: Point = index === inkData.length - 1 ? lastEnd[0] : inkData[index + 1];
+ if (prevPt.X === nextPt.X) {
+ const verticalDist = nextPt.Y - prevPt.Y;
+ return [{ X: pt.X, Y: pt.Y - alpha * verticalDist }, pt, pt, { X: pt.X, Y: pt.Y + alpha * verticalDist }];
+ } else if (prevPt.Y === nextPt.Y) {
+ const horizDist = nextPt.X - prevPt.X;
+ return [{ X: pt.X - alpha * horizDist, Y: pt.Y }, pt, pt, { X: pt.X + alpha * horizDist, Y: pt.Y }];
+ }
+ // tangent vectors between the adjacent points
+ const tanX = nextPt.X - prevPt.X;
+ const tanY = nextPt.Y - prevPt.Y;
+ const ctrlPt1: Point = { X: pt.X - alpha * tanX, Y: pt.Y - alpha * tanY };
+ const ctrlPt2: Point = { X: pt.X + alpha * tanX, Y: pt.Y + alpha * tanY };
+ return [ctrlPt1, pt, pt, ctrlPt2];
+ });
+ return [...firstEnd, ...points, ...lastEnd];
+}
+
+export function SVGToBezier(name: SVGType, attributes: any): Point[] {
+ switch (name) {
+ case 'line':
+ const x1 = parseInt(attributes.x1);
+ const x2 = parseInt(attributes.x2);
+ const y1 = parseInt(attributes.y1);
+ const y2 = parseInt(attributes.y2);
+ return [
+ { X: x1, Y: y1 },
+ { X: x1, Y: y1 },
+ { X: x2, Y: y2 },
+ { X: x2, Y: y2 },
+ ];
+ case 'circle':
+ case 'ellipse':
+ const c = 0.551915024494;
+ const centerX = parseInt(attributes.cx);
+ const centerY = parseInt(attributes.cy);
+ const radiusX = parseInt(attributes.rx) || parseInt(attributes.r);
+ const radiusY = parseInt(attributes.ry) || parseInt(attributes.r);
+ return [
+ { X: centerX, Y: centerY + radiusY },
+ { X: centerX + c * radiusX, Y: centerY + radiusY },
+ { X: centerX + radiusX, Y: centerY + c * radiusY },
+ { X: centerX + radiusX, Y: centerY },
+ { X: centerX + radiusX, Y: centerY },
+ { X: centerX + radiusX, Y: centerY - c * radiusY },
+ { X: centerX + c * radiusX, Y: centerY - radiusY },
+ { X: centerX, Y: centerY - radiusY },
+ { X: centerX, Y: centerY - radiusY },
+ { X: centerX - c * radiusX, Y: centerY - radiusY },
+ { X: centerX - radiusX, Y: centerY - c * radiusY },
+ { X: centerX - radiusX, Y: centerY },
+ { X: centerX - radiusX, Y: centerY },
+ { X: centerX - radiusX, Y: centerY + c * radiusY },
+ { X: centerX - c * radiusX, Y: centerY + radiusY },
+ { X: centerX, Y: centerY + radiusY },
+ ];
+ case 'rect':
+ const x = parseInt(attributes.x);
+ const y = parseInt(attributes.y);
+ const width = parseInt(attributes.width);
+ const height = parseInt(attributes.height);
+ return [
+ { X: x, Y: y },
+ { X: x, Y: y },
+ { X: x + width, Y: y },
+ { X: x + width, Y: y },
+ { X: x + width, Y: y },
+ { X: x + width, Y: y },
+ { X: x + width, Y: y + height },
+ { X: x + width, Y: y + height },
+ { X: x + width, Y: y + height },
+ { X: x + width, Y: y + height },
+ { X: x, Y: y + height },
+ { X: x, Y: y + height },
+ { X: x, Y: y + height },
+ { X: x, Y: y + height },
+ { X: x, Y: y },
+ { X: x, Y: y },
+ ];
+ case 'path':
+ const coordList: Point[] = [];
+ const startPt = attributes.d.match(/M(-?\d+\.?\d*),(-?\d+\.?\d*)/);
+ coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
+ const matches: RegExpMatchArray[] = Array.from(
+ attributes.d.matchAll(/Q(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|C(-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*) (-?\d+\.?\d*),(-?\d+\.?\d*)|L(-?\d+\.?\d*),(-?\d+\.?\d*)/g)
+ );
+ let lastPt: Point;
+ matches.forEach(match => {
+ if (match[0].startsWith('Q')) {
+ coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
+ coordList.push({ X: parseInt(match[1]), Y: parseInt(match[2]) });
+ coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) });
+ coordList.push({ X: parseInt(match[3]), Y: parseInt(match[4]) });
+ lastPt = { X: parseInt(match[3]), Y: parseInt(match[4]) };
+ } else if (match[0].startsWith('C')) {
+ coordList.push({ X: parseInt(match[5]), Y: parseInt(match[6]) });
+ coordList.push({ X: parseInt(match[7]), Y: parseInt(match[8]) });
+ coordList.push({ X: parseInt(match[9]), Y: parseInt(match[10]) });
+ coordList.push({ X: parseInt(match[9]), Y: parseInt(match[10]) });
+ lastPt = { X: parseInt(match[9]), Y: parseInt(match[10]) };
+ } else {
+ coordList.push(lastPt || { X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
+ coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) });
+ coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) });
+ coordList.push({ X: parseInt(match[11]), Y: parseInt(match[12]) });
+ lastPt = { X: parseInt(match[11]), Y: parseInt(match[12]) };
+ }
+ });
+ const hasZ = attributes.d.match(/Z/);
+ if (hasZ) {
+ coordList.push(lastPt);
+ coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
+ coordList.push({ X: parseInt(startPt[1]), Y: parseInt(startPt[2]) });
+ } else {
+ coordList.pop();
+ }
+ return coordList;
+ case 'polygon':
+ default:
+ return [];
+ }
+}
+
/*
static double GetTValueFromSValue (const BezierRep &parent, double t, double endT, bool left, double influenceDistance, double &excess) {
double dist = 0;
diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx
index 487868169..eb0b00472 100644
--- a/src/client/views/DocumentButtonBar.tsx
+++ b/src/client/views/DocumentButtonBar.tsx
@@ -12,7 +12,7 @@ import * as React from 'react';
import { FaEdit } from 'react-icons/fa';
import { returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick } from '../../ClientUtils';
import { emptyFunction } from '../../Utils';
-import { Doc } from '../../fields/Doc';
+import { Doc, DocListCast } from '../../fields/Doc';
import { Cast, DocCast } from '../../fields/Types';
import { DocUtils, IsFollowLinkScript } from '../documents/DocUtils';
import { CalendarManager } from '../util/CalendarManager';
@@ -31,6 +31,8 @@ import { DocumentLinksButton } from './nodes/DocumentLinksButton';
import { DocumentView } from './nodes/DocumentView';
import { OpenWhere } from './nodes/OpenWhere';
import { DashFieldView } from './nodes/formattedText/DashFieldView';
+import { AnnotationPalette } from './smartdraw/AnnotationPalette';
+import { DocData } from '../../fields/DocSymbols';
@observer
export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (DocumentView | undefined)[]; stack?: any }> {
@@ -241,6 +243,28 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
);
}
+ @observable _annoSaved: boolean = false;
+
+ @undoBatch
+ saveAnno = action((targetDoc: Doc) => {
+ // targetDoc.savedAsAnno = true;
+ this._annoSaved = true;
+ AnnotationPalette.Instance.addToPalette(targetDoc);
+ });
+
+ @computed
+ get saveAnnoButton() {
+ const targetDoc = this.view0?.Document;
+ if (targetDoc && targetDoc.savedAsAnno) this._annoSaved = true;
+ return !targetDoc ? null : (
+ <Tooltip title={<div className="dash-tooltip">{this._annoSaved ? 'Saved as Annotation!' : 'Save to Annotation Palette'}</div>}>
+ <div className="documentButtonBar-icon" style={{ color: 'white' }} onClick={() => this.saveAnno(targetDoc)}>
+ <FontAwesomeIcon className="documentdecorations-icon" icon={this._annoSaved ? 'clipboard-check' : 'file-arrow-down'} />
+ </div>
+ </Tooltip>
+ );
+ }
+
@computed
get shareButton() {
const targetDoc = this.view0?.Document;
@@ -450,6 +474,7 @@ export class DocumentButtonBar extends ObservableReactComponent<{ views: () => (
<div className="documentButtonBar-button">{this.templateButton}</div>
{!DocumentView.Selected().some(v => v.allLinks.length) ? null : <div className="documentButtonBar-button">{this.followLinkButton}</div>}
<div className="documentButtonBar-button">{this.pinButton}</div>
+ <div className="documentButtonBar-button">{this.saveAnnoButton}</div>
<div className="documentButtonBar-button">{this.recordButton}</div>
<div className="documentButtonBar-button">{this.calendarButton}</div>
{!Doc.UserDoc().documentLinksButton_fullMenu ? null : <div className="documentButtonBar-button">{this.shareButton}</div>}
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index e961bc031..649208989 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -248,15 +248,15 @@ export class GestureOverlay extends ObservableReactComponent<React.PropsWithChil
this._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: left, Y: top }); // curr pt
+ this._points.push({ X: left, Y: top }); // curr first ctrl pt
+ this._points.push({ X: right, Y: top }); // next ctrl pt
+ this._points.push({ X: right, Y: top }); // next pt
- this._points.push({ X: right, Y: top });
- 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: top }); // next pt
+ this._points.push({ X: right, Y: top }); // next first ctrl pt
+ this._points.push({ X: right, Y: bottom }); // next next ctrl pt
+ this._points.push({ X: right, Y: bottom }); // next next pt
this._points.push({ X: right, Y: bottom });
this._points.push({ X: right, Y: bottom });
@@ -299,13 +299,13 @@ 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, Y: centerY + radius }); // curr pt
+ this._points.push({ X: centerX + c * radius, Y: centerY + radius }); // curr first ctrl pt
+ this._points.push({ X: centerX + radius, Y: centerY + c * radius }); // next pt ctrl pt
+ this._points.push({ X: centerX + radius, Y: centerY }); // next pt
- this._points.push({ X: centerX + radius, Y: centerY });
- this._points.push({ X: centerX + radius, Y: centerY - c * radius });
+ this._points.push({ X: centerX + radius, Y: centerY }); // next pt
+ this._points.push({ X: centerX + radius, Y: centerY - c * radius }); // next first ctrl pt
this._points.push({ X: centerX + c * radius, Y: centerY - radius });
this._points.push({ X: centerX, Y: centerY - radius });
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 7d01bbabb..562827db5 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -162,7 +162,7 @@ export class KeyManager {
case 'delete':
case 'backspace':
if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') {
- if (DocumentView.LightboxDoc()) {
+ if (DocumentView.LightboxDoc() && !DocumentView.Selected().length) {
DocumentView.SetLightboxDoc(undefined);
DocumentView.DeselectAll();
} else if (!window.getSelection()?.toString()) DocumentDecorations.Instance.onCloseClick(true);
diff --git a/src/client/views/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 7198c7f05..4fcb7ec9c 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -23,6 +23,8 @@ import { DocumentView } from './nodes/DocumentView';
import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
import { OverlayView } from './OverlayView';
+import { AnnotationPalette } from './smartdraw/AnnotationPalette';
+import { DocData } from '../../fields/DocSymbols';
interface LightboxViewProps {
PanelWidth: number;
@@ -40,7 +42,14 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
* @param view
* @returns true if a DocumentView is descendant of the lightbox view
*/
- public static Contains(view?:DocumentView) { return view && LightboxView.Instance?._docView && (view.containerViewPath?.() ?? []).concat(view).includes(LightboxView.Instance?._docView); } // prettier-ignore
+ public static Contains(view?: DocumentView) {
+ return true;
+ }
+ // return (
+ // (view && LightboxView.Instance?._docView && (view.containerViewPath?.() ?? []).concat(view).includes(LightboxView.Instance?._docView)) ||
+ // view?.Document === AnnotationPalette.Instance.FreeformCanvas ||
+ // view?.Document.embedContainer === AnnotationPalette.Instance.DrawingCarousel
+ // ); } // prettier-ignore
public static LightboxDoc = () => LightboxView.Instance?._doc;
// eslint-disable-next-line no-use-before-define
static Instance: LightboxView;
@@ -59,6 +68,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
@observable private _doc: Opt<Doc> = undefined;
@observable private _docTarget: Opt<Doc> = undefined;
@observable private _docView: Opt<DocumentView> = undefined;
+ @observable private _showPalette: boolean = false;
@computed get leftBorder() { return Math.min(this._props.PanelWidth / 4, this._props.maxBorder[0]); } // prettier-ignore
@computed get topBorder() { return Math.min(this._props.PanelHeight / 4, this._props.maxBorder[1]); } // prettier-ignore
@@ -71,6 +81,7 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
DocumentView._lightboxContains = LightboxView.Contains;
DocumentView._lightboxDoc = LightboxView.LightboxDoc;
}
+
/**
* Sets the root Doc to render in the lightbox view.
* @param doc
@@ -103,6 +114,8 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
this._history = [];
Doc.ActiveTool = InkTool.None;
SnappingManager.SetExploreMode(false);
+ AnnotationPalette.Instance.displayPalette(false);
+ this._showPalette = false;
}
DocumentView.DeselectAll();
if (future) {
@@ -202,6 +215,11 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
toggleFitWidth = () => {
this._doc && (this._doc._layout_fitWidth = !this._doc._layout_fitWidth);
};
+ togglePalette = () => {
+ this._showPalette = !this._showPalette;
+ AnnotationPalette.Instance.displayPalette(this._showPalette);
+ if (this._showPalette === false) AnnotationPalette.Instance.resetPalette(true);
+ };
togglePen = () => {
Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
};
@@ -318,7 +336,8 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
)}
<LightboxTourBtn lightboxDoc={this.lightboxDoc} navBtn={this.renderNavBtn} future={this.future} stepInto={this.stepInto} />
{toggleBtn('lightboxView-navBtn', 'toggle reading view', this._doc?._layout_fitWidth, 'book-open', 'book', this.toggleFitWidth)}
- {toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-download', '', this.downloadDoc)}
+ {toggleBtn('lightboxView-tabBtn', 'open document in a tab', false, 'file-export', '', this.downloadDoc)}
+ {toggleBtn('lightboxView-paletteBtn', 'toggle annotation palette', this._showPalette === true, 'palette', '', this.togglePalette)}
{toggleBtn('lightboxView-penBtn', 'toggle pen annotation', Doc.ActiveTool === InkTool.Pen, 'pen', '', this.togglePen)}
{toggleBtn('lightboxView-exploreBtn', 'toggle navigate only mode', SnappingManager.ExploreMode, 'globe-americas', '', this.toggleExplore)}
</div>
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index f8c7fd7b1..fd1af7547 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -76,6 +76,8 @@ import { PresBox } from './nodes/trails';
import { AnchorMenu } from './pdf/AnchorMenu';
import { GPTPopup } from './pdf/GPTPopup/GPTPopup';
import { TopBar } from './topbar/TopBar';
+import { SmartDrawHandler } from './smartdraw/SmartDrawHandler';
+import { AnnotationPalette } from './smartdraw/AnnotationPalette';
import { InkTranscription } from './InkTranscription';
const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
@@ -318,6 +320,7 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faCompass,
fa.faSnowflake,
fa.faStar,
+ fa.faSplotch,
fa.faMicrophone,
fa.faCircleHalfStroke,
fa.faKeyboard,
@@ -340,6 +343,7 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faTerminal,
fa.faToggleOn,
fa.faFile,
+ fa.faFileExport,
fa.faLocationArrow,
fa.faSearch,
fa.faFileDownload,
@@ -379,6 +383,7 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faXmark,
fa.faExclamation,
fa.faFileAlt,
+ fa.faFileArrowDown,
fa.faFileAudio,
fa.faFileVideo,
fa.faFilePdf,
@@ -395,6 +400,7 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faArrowsLeftRight,
fa.faPause,
fa.faPen,
+ fa.faUserPen,
fa.faPenNib,
fa.faPhone,
fa.faPlay,
@@ -432,6 +438,7 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faBold,
fa.faItalic,
fa.faClipboard,
+ fa.faClipboardCheck,
fa.faUnderline,
fa.faStrikethrough,
fa.faSuperscript,
@@ -440,6 +447,7 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faEyeDropper,
fa.faPaintRoller,
fa.faBars,
+ fa.faBarsStaggered,
fa.faBrush,
fa.faShapes,
fa.faEllipsisH,
@@ -478,6 +486,8 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faHashtag,
fa.faAlignJustify,
fa.faCheckSquare,
+ fa.faSquarePlus,
+ fa.faReply,
fa.faListUl,
fa.faWindowMinimize,
fa.faWindowRestore,
@@ -573,6 +583,9 @@ export class MainView extends ObservableReactComponent<{}> {
Doc.linkFollowUnhighlight();
AudioBox.Enabled = true;
const targets = document.elementsFromPoint(e.x, e.y);
+ const targetClasses: string[] = targets.map(target => {
+ return target.className.toString();
+ });
if (targets.length) {
let targClass = targets[0].className.toString();
for (let i = 0; i < targets.length - 1; i++) {
@@ -580,6 +593,8 @@ export class MainView extends ObservableReactComponent<{}> {
else break;
}
!targClass.includes('contextMenu') && ContextMenu.Instance.closeMenu();
+ !targetClasses.includes('marqueeView') && !targetClasses.includes('smart-draw-handler') && SmartDrawHandler.Instance.hideSmartDrawHandler();
+ !targetClasses.includes('smart-draw-handler') && SmartDrawHandler.Instance.hideRegenerate();
!['timeline-menu-desc', 'timeline-menu-item', 'timeline-menu-input'].includes(targClass) && TimelineMenu.Instance.closeMenu();
}
});
@@ -1090,6 +1105,7 @@ export class MainView extends ObservableReactComponent<{}> {
<TaskCompletionBox />
<ContextMenu />
<ImageLabelHandler />
+ <SmartDrawHandler />
<AnchorMenu />
<MapAnchorMenu />
<DirectionsAnchorMenu />
@@ -1103,6 +1119,7 @@ export class MainView extends ObservableReactComponent<{}> {
<GPTPopup key="gptpopup" />
<SchemaCSVPopUp key="schemacsvpopup" />
<GenerativeFill imageEditorOpen={ImageEditor.Open} imageEditorSource={ImageEditor.Source} imageRootDoc={ImageEditor.RootDoc} addDoc={ImageEditor.AddDoc} />
+ <AnnotationPalette />
</div>
);
}
diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx
index c18ac6738..f06f3efe0 100644
--- a/src/client/views/MarqueeAnnotator.tsx
+++ b/src/client/views/MarqueeAnnotator.tsx
@@ -28,6 +28,7 @@ export interface MarqueeAnnotatorProps {
marqueeContainer: HTMLDivElement;
docView: () => DocumentView;
savedAnnotations: () => ObservableMap<number, HTMLDivElement[]>;
+ savedTapes: () => ObservableMap<number, HTMLDivElement[]>;
selectionText: () => string;
annotationLayer: HTMLDivElement;
addDocument: (doc: Doc) => boolean;
@@ -74,6 +75,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
backgroundColor: color,
annotationOn: this.props.Document,
title: 'Annotation on ' + this.props.Document.title,
+ a,
});
marqueeAnno.x = NumCast(doc.freeform_panX_min) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale;
marqueeAnno.y = NumCast(doc.freeform_panY_min) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale;
@@ -127,6 +129,140 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
savedAnnoMap.clear();
return textRegionAnno;
};
+
+ // @undoBatch
+ // makeTapeDocument = (color: string, isLinkButton?: boolean, savedTapes?: ObservableMap<number, HTMLDivElement[]>): Opt<Doc> => {
+ // const savedTapeMap = savedTapes?.values() && Array.from(savedTapes?.values()).length ? savedTapes : this.props.savedTapes();
+ // if (savedTapeMap.size === 0) return undefined;
+ // const tapes = Array.from(savedTapeMap.values())[0];
+ // const doc = this.props.Document;
+ // const scale = (this.props.annotationLayerScaling?.() || 1) * NumCast(doc._freeform_scale, 1);
+ // if (tapes.length && (tapes[0] as any).marqueeing) {
+ // const anno = tapes[0];
+ // const containerOffset = this.props.containerOffset?.() || [0, 0];
+ // const tape = Docs.Create.FreeformDocument([], {
+ // onClick: isLinkButton ? FollowLinkScript() : undefined,
+ // backgroundColor: color,
+ // annotationOn: this.props.Document,
+ // title: 'Tape on ' + this.props.Document.title,
+ // });
+ // tape.x = NumCast(doc.freeform_panX_min) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale;
+ // tape.y = NumCast(doc.freeform_panY_min) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale;
+ // tape._height = parseInt(anno.style.height || '0') / scale;
+ // tape._width = parseInt(anno.style.width || '0') / scale;
+ // anno.remove();
+ // savedTapeMap.clear();
+ // return tape;
+ // }
+
+ // const textRegionAnno = Docs.Create.ConfigDocument({
+ // annotationOn: this.props.Document,
+ // text: this.props.selectionText() as any, // text want an RTFfield, but strings are acceptable, too.
+ // text_html: this.props.selectionText() as any,
+ // backgroundColor: 'transparent',
+ // presentation_duration: 2100,
+ // presentation_transition: 500,
+ // presentation_zoomText: true,
+ // title: '>' + this.props.Document.title,
+ // });
+ // const textRegionAnnoProto = textRegionAnno[DocData];
+ // let minX = Number.MAX_VALUE;
+ // let maxX = -Number.MAX_VALUE;
+ // let minY = Number.MAX_VALUE;
+ // let maxY = -Number.MIN_VALUE;
+ // const annoRects: string[] = [];
+ // savedAnnoMap.forEach((value: HTMLDivElement[]) =>
+ // value.forEach(anno => {
+ // const x = parseInt(anno.style.left ?? '0');
+ // const y = parseInt(anno.style.top ?? '0');
+ // const height = parseInt(anno.style.height ?? '0');
+ // const width = parseInt(anno.style.width ?? '0');
+ // annoRects.push(`${x}:${y}:${width}:${height}`);
+ // anno.remove();
+ // minY = Math.min(NumCast(y), minY);
+ // minX = Math.min(NumCast(x), minX);
+ // maxY = Math.max(NumCast(y) + NumCast(height), maxY);
+ // maxX = Math.max(NumCast(x) + NumCast(width), maxX);
+ // })
+ // );
+
+ // textRegionAnnoProto.y = Math.max(minY, 0);
+ // textRegionAnnoProto.x = Math.max(minX, 0);
+ // textRegionAnnoProto.height = Math.max(maxY, 0) - Math.max(minY, 0);
+ // textRegionAnnoProto.width = Math.max(maxX, 0) - Math.max(minX, 0);
+ // textRegionAnnoProto.backgroundColor = color;
+ // // mainAnnoDocProto.text = this._selectionText;
+ // textRegionAnnoProto.text_inlineAnnotations = new List<string>(annoRects);
+ // textRegionAnnoProto.opacity = 0;
+ // textRegionAnnoProto.layout_unrendered = true;
+ // savedAnnoMap.clear();
+ // return textRegionAnno;
+ // };
+
+ @undoBatch
+ makeTapeDocument = (color: string, isLinkButton?: boolean, savedTapes?: ObservableMap<number, HTMLDivElement[]>): Opt<Doc> => {
+ // const savedAnnoMap = savedTapes?.values() && Array.from(savedTapes?.values()).length ? savedTapes : this.props.savedTapes();
+ // if (savedAnnoMap.size === 0) return undefined;
+ // const savedAnnos = Array.from(savedAnnoMap.values())[0];
+ const doc = this.props.Document;
+ const scale = (this.props.annotationLayerScaling?.() || 1) * NumCast(doc._freeform_scale, 1);
+ const marqueeAnno = Docs.Create.FreeformDocument([], {
+ onClick: isLinkButton ? FollowLinkScript() : undefined,
+ backgroundColor: color,
+ annotationOn: this.props.Document,
+ title: 'Annotation on ' + this.props.Document.title,
+ });
+ marqueeAnno.x = NumCast(doc.freeform_panX_min) / scale;
+ marqueeAnno.y = NumCast(doc.freeform_panY_min) / scale;
+ marqueeAnno._height = parseInt('100') / scale;
+ marqueeAnno._width = parseInt('100') / scale;
+ return marqueeAnno;
+ // }
+
+ // const textRegionAnno = Docs.Create.ConfigDocument({
+ // annotationOn: this.props.Document,
+ // text: this.props.selectionText() as any, // text want an RTFfield, but strings are acceptable, too.
+ // text_html: this.props.selectionText() as any,
+ // backgroundColor: 'transparent',
+ // presentation_duration: 2100,
+ // presentation_transition: 500,
+ // presentation_zoomText: true,
+ // title: '>' + this.props.Document.title,
+ // });
+ // const textRegionAnnoProto = textRegionAnno[DocData];
+ // let minX = Number.MAX_VALUE;
+ // let maxX = -Number.MAX_VALUE;
+ // let minY = Number.MAX_VALUE;
+ // let maxY = -Number.MIN_VALUE;
+ // const annoRects: string[] = [];
+ // savedAnnoMap.forEach((value: HTMLDivElement[]) =>
+ // value.forEach(anno => {
+ // const x = parseInt(anno.style.left ?? '0');
+ // const y = parseInt(anno.style.top ?? '0');
+ // const height = parseInt(anno.style.height ?? '0');
+ // const width = parseInt(anno.style.width ?? '0');
+ // annoRects.push(`${x}:${y}:${width}:${height}`);
+ // anno.remove();
+ // minY = Math.min(NumCast(y), minY);
+ // minX = Math.min(NumCast(x), minX);
+ // maxY = Math.max(NumCast(y) + NumCast(height), maxY);
+ // maxX = Math.max(NumCast(x) + NumCast(width), maxX);
+ // })
+ // );
+
+ // textRegionAnnoProto.y = Math.max(minY, 0);
+ // textRegionAnnoProto.x = Math.max(minX, 0);
+ // textRegionAnnoProto.height = Math.max(maxY, 0) - Math.max(minY, 0);
+ // textRegionAnnoProto.width = Math.max(maxX, 0) - Math.max(minX, 0);
+ // textRegionAnnoProto.backgroundColor = color;
+ // // mainAnnoDocProto.text = this._selectionText;
+ // textRegionAnnoProto.text_inlineAnnotations = new List<string>(annoRects);
+ // textRegionAnnoProto.opacity = 0;
+ // textRegionAnnoProto.layout_unrendered = true;
+ // savedAnnoMap.clear();
+ // return textRegionAnno;
+ };
+
@action
highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => {
// creates annotation documents for current highlights
@@ -136,6 +272,15 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
return annotationDoc as Doc;
};
+ @action
+ tape = (color: string, isLinkButton: boolean, savedTapes?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean) => {
+ // creates annotation documents for current highlights
+ const effectiveAcl = GetEffectiveAcl(this.props.Document[DocData]);
+ const tape = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeTapeDocument(color, isLinkButton, savedTapes);
+ addAsAnnotation && tape && this.props.addDocument(tape);
+ return tape as Doc;
+ };
+
public static previewNewAnnotation = action((savedAnnotations: ObservableMap<number, HTMLDivElement[]>, annotationLayer: HTMLDivElement, div: HTMLDivElement, page: number) => {
div.style.backgroundColor = '#ACCEF7';
div.style.opacity = '0.5';
@@ -182,6 +327,7 @@ export class MarqueeAnnotator extends ObservableReactComponent<MarqueeAnnotatorP
AnchorMenu.Instance.OnClick = undoable(() => this.props.anchorMenuClick?.()?.(this.highlight(this.props.highlightDragSrcColor ?? 'rgba(173, 216, 230, 0.75)', true, undefined, true)), 'make sidebar annotation');
AnchorMenu.Instance.OnAudio = unimplementedFunction;
AnchorMenu.Instance.Highlight = (color: string) => this.highlight(color, false, undefined, true);
+ AnchorMenu.Instance.Tape = (color: string) => this.tape(color, false, undefined, true);
AnchorMenu.Instance.GetAnchor = (savedAnnotations?: ObservableMap<number, HTMLDivElement[]> /* , addAsAnnotation?: boolean */) => this.highlight('rgba(173, 216, 230, 0.75)', true, savedAnnotations, true);
AnchorMenu.Instance.onMakeAnchor = () => AnchorMenu.Instance.GetAnchor(undefined, true);
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index b7f8a3170..8c100f238 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -152,7 +152,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps &
case StyleProp.DocContents: return undefined;
case StyleProp.WidgetColor: return isAnnotated ? Colors.LIGHT_BLUE : 'dimgrey';
case StyleProp.Opacity: return componentView?.isUnstyledView?.() ? 1 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null));
- case StyleProp.FontColor: return StrCast(doc?.[fieldKey + 'fontColor'], StrCast(Doc.UserDoc().fontColor, color()));
+ case StyleProp.FontColor: return StrCast(doc?.[fieldKey + 'fontColor'], isCaption ? lightOrDark(backgroundCol()) : StrCast(Doc.UserDoc().fontColor, color()));
case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(Doc.UserDoc().fontSize));
case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(Doc.UserDoc().fontFamily));
case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(Doc.UserDoc().fontWeight));
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/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index d611db1f8..d0f65866b 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,7 +1,7 @@
/* eslint-disable react/jsx-props-no-spreading */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/no-static-element-interactions */
-import { Bezier } from 'bezier-js';
+import { Bezier, Point } from 'bezier-js';
import { Colors } from 'browndash-components';
import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
@@ -27,6 +27,7 @@ import { aggregateBounds, clamp, emptyFunction, intersectRect, Utils } from '../
import { Docs } from '../../../documents/Documents';
import { CollectionViewType, DocumentType } from '../../../documents/DocumentTypes';
import { DocUtils } from '../../../documents/DocUtils';
+import { FitCurve, GenerateControlPoints } from '../../../util/bezierFit';
import { DragManager } from '../../../util/DragManager';
import { dropActionType } from '../../../util/DropActionTypes';
import { CompileScript } from '../../../util/Scripting';
@@ -55,6 +56,8 @@ import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannable
import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
import './CollectionFreeFormView.scss';
import { MarqueeView } from './MarqueeView';
+import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
+import { AnnotationPalette } from '../../smartdraw/AnnotationPalette';
@observer
class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
@@ -118,6 +121,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>();
@@ -497,28 +501,30 @@ 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, hit !== -1);
+ e.stopPropagation();
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);
}
break;
- default:
+ default:
}
}
}
@@ -569,6 +575,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
}
};
+
@action
onEraserUp = (): void => {
this._deleteList.lastElement()?._props.removeDocument?.(this._deleteList.map(ink => ink.Document));
@@ -609,61 +616,92 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
_eraserLock = 0;
_eraserPts: number[][] = []; // keep track of the last few eraserPts to make the eraser circle 'stretch'
- /**
- * 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.
- */
- @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',
+ 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[])
+ )
+ );
+ }
+ 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[]);
+ 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--);
+ inkWidth,
+ ActiveInkColor(),
+ ActiveInkBezierApprox(),
+ ActiveFillColor(),
+ ActiveArrowStart(),
+ ActiveArrowEnd(),
+ ActiveDash(),
+ ActiveIsInkMask()
+ );
+ });
+ newStrokes && this.addDocument?.(newStrokes);
+ // setTimeout(() => this._eraserLock--);
+ }
}
- // Lower ink opacity to give the user a visual indicator of deletion.
- intersect.inkView.layoutDoc.opacity = 0;
- intersect.inkView.layoutDoc.dontIntersect = true;
- }
- });
+ });
+ }
return false;
};
/**
+ * 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.
+ */
+ @action
+ 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;
+ };
+
+ @action
+ onEraserClick = (e: PointerEvent, doubleTap?: boolean) => {
+ e.preventDefault();
+ e.stopImmediatePropagation();
+ this.erase(e, [0, 0]);
+ };
+
+ /**
* 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.
@@ -672,32 +710,32 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
* @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;
- });
- return false;
- };
+ // @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;
+ // });
+ // return false;
+ // };
forceStrokeGesture = (e: PointerEvent, gesture: Gestures, points: InkData, text?: any) => {
this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, InkField.getBounds(points), text));
@@ -728,7 +766,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
@@ -1239,6 +1277,69 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
};
@action
+ showSmartDraw = (e: PointerEvent, doubleTap?: boolean) => {
+ SmartDrawHandler.Instance.displaySmartDrawHandler(e.pageX, e.pageY, this.createDrawing, this.removeDrawing);
+ };
+
+ _drawing: Doc[] = [];
+ _drawingContainer: Doc | undefined = undefined;
+ @undoBatch
+ createDrawing = (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => {
+ this._drawing = [];
+ const xf = this.screenToFreeformContentsXf;
+ // this._drawingContainer = undefined;
+ strokeData.forEach((stroke: [InkData, string, string]) => {
+ const bounds = InkField.getBounds(stroke[0]);
+ const B = xf.transformBounds(bounds.left, bounds.top, bounds.width, bounds.height);
+ const inkWidth = ActiveInkWidth() * this.ScreenToLocalBoxXf().Scale;
+ const inkDoc = Docs.Create.InkDocument(
+ stroke[0],
+ { title: 'stroke',
+ x: B.x - inkWidth / 2,
+ y: B.y - inkWidth / 2,
+ _width: B.width + inkWidth,
+ _height: B.height + inkWidth,
+ stroke_showLabel: BoolCast(Doc.UserDoc().activeInkHideTextLabels)}, // prettier-ignore
+ inkWidth,
+ stroke[1],
+ ActiveInkBezierApprox(),
+ stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
+ ActiveArrowStart(),
+ ActiveArrowEnd(),
+ ActiveDash(),
+ ActiveIsInkMask()
+ );
+ this._drawing.push(inkDoc);
+ this.addDocument(inkDoc);
+ });
+ const collection = containerDoc || this._marqueeViewRef.current?.collection(undefined, true, this._drawing);
+ if (collection) {
+ const docData = collection[DocData];
+ docData.title = opts.text.match(/^(.*?)~~~.*$/)?.[1] || opts.text;
+ docData.drawingInput = opts.text;
+ docData.drawingComplexity = opts.complexity;
+ docData.drawingColored = opts.autoColor;
+ docData.drawingSize = opts.size;
+ docData.drawingData = gptRes;
+ this._drawingContainer = collection;
+ }
+ this._batch?.end();
+ };
+
+ removeDrawing = (doc?: Doc) => {
+ this._batch = UndoManager.StartBatch('regenerateDrawing');
+ if (doc) {
+ const docData = DocCast(doc[DocData]);
+ const children = DocListCast(docData.data);
+ this._props.removeDocument?.(children);
+ // this._props.removeDocument?.(doc);
+ } else {
+ this._props.removeDocument?.(this._drawing);
+ // if (this._drawingContainer) this._props.removeDocument?.(this._drawingContainer);
+ }
+ };
+
+ @action
zoom = (pointX: number, pointY: number, deltaY: number): void => {
if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05;
@@ -1831,8 +1932,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
@action
onCursorMove = (e: React.PointerEvent) => {
- this._eraserX = e.clientX;
- this._eraserY = e.clientY;
+ const locPt = this.ScreenToLocalBoxXf().transformPoint(e.clientX, e.clientY);
+ this._eraserX = locPt[0];
+ this._eraserY = locPt[1];
+ // Doc.ActiveTool === InkTool.RadiusEraser ? this._childPointerEvents = 'none' : this._childPointerEvents = 'all'
// super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY));
};
@@ -1939,6 +2042,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}),
icon: 'eye',
});
+ optionItems.push({
+ description: 'Show Drawing Editor',
+ event: action(() => {
+ !SmartDrawHandler.Instance._showRegenerate ? SmartDrawHandler.Instance.displayRegenerate(this._downX, this._downY - 10, this.createDrawing, this.removeDrawing) : SmartDrawHandler.Instance.hideRegenerate();
+ }),
+ icon: 'pen-to-square',
+ });
this._props.renderDepth &&
optionItems.push({
description: 'Use Background Color as Default',
@@ -2143,8 +2253,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 f02cd9d45..b3fdd9379 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeOptionsMenu.tsx
@@ -3,6 +3,7 @@ import { IconButton } from 'browndash-components';
import { computed, makeObservable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
+import { Doc } from '../../../../fields/Doc';
import { unimplementedFunction } from '../../../../Utils';
import { SettingsManager } from '../../../util/SettingsManager';
import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu';
@@ -12,7 +13,7 @@ export class MarqueeOptionsMenu extends AntimodeMenu<AntimodeMenuProps> {
// eslint-disable-next-line no-use-before-define
static Instance: MarqueeOptionsMenu;
- public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean) => void = unimplementedFunction;
+ public createCollection: (e: KeyboardEvent | React.PointerEvent | undefined, group?: boolean, selection?: Doc[]) => Doc | void = unimplementedFunction;
public delete: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public summarize: (e: KeyboardEvent | React.PointerEvent | undefined) => void = unimplementedFunction;
public showMarquee: () => void = unimplementedFunction;
diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
index dc15c83c5..5aff3ed6f 100644
--- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
+++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx
@@ -36,6 +36,7 @@ import { CollectionFreeFormView } from './CollectionFreeFormView';
import { ImageLabelHandler } from './ImageLabelHandler';
import { MarqueeOptionsMenu } from './MarqueeOptionsMenu';
import './MarqueeView.scss';
+import { collectionOf } from '@turf/turf';
interface MarqueeViewProps {
getContainerTransform: () => Transform;
@@ -374,8 +375,8 @@ 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 = this.Bounds.width || 1; // if width/height are unset/0, then groups won't autoexpand to contain their children
+ newCollection._height = this.Bounds.height || 1;
newCollection._dragWhenActive = makeGroup;
newCollection.x = this.Bounds.left;
newCollection.y = this.Bounds.top;
@@ -426,6 +427,7 @@ export class MarqueeView extends ObservableReactComponent<SubCollectionViewProps
this._props.selectDocuments([newCollection]);
MarqueeOptionsMenu.Instance.fadeOut(true);
this.hideMarquee();
+ return newCollection;
});
/**
diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts
index bba34e302..bda3d0ebb 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -508,9 +508,9 @@ 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.toString());},
+ setMode: () => { SetEraserWidth(value);},
}]
]);
diff --git a/src/client/views/nodes/PDFBox.scss b/src/client/views/nodes/PDFBox.scss
index 7bca1230f..6e24b2931 100644
--- a/src/client/views/nodes/PDFBox.scss
+++ b/src/client/views/nodes/PDFBox.scss
@@ -20,15 +20,27 @@
top: 0;
left: 0;
+ .pdfBox-sidebarBtn-container {
+ display: flex;
+ flex-direction: row;
+ position: absolute;
+ width: 53px;
+ height: 33px;
+ right: 5px;
+ align-items: center;
+ justify-content: space-between;
+ z-index: 1;
+ }
+
// glr: This should really be the same component as text and PDFs
.pdfBox-sidebarBtn {
background: $black;
height: 25px;
width: 25px;
- right: 5px;
+ // right: 5px;
color: $white;
display: flex;
- position: absolute;
+ // position: absolute;
align-items: center;
justify-content: center;
border-radius: 3px;
diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx
index 8db68ddfe..782df99f6 100644
--- a/src/client/views/nodes/PDFBox.tsx
+++ b/src/client/views/nodes/PDFBox.tsx
@@ -1,6 +1,8 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/control-has-associated-label */
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { IconButton } from 'browndash-components';
+import { black } from 'colors';
import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx';
import { observer } from 'mobx-react';
import * as Pdfjs from 'pdfjs-dist';
@@ -503,17 +505,30 @@ export class PDFBox extends ViewBoxAnnotatableComponent<FieldViewProps>() {
}
@computed get sidebarHandle() {
return (
- <div
- className="pdfBox-sidebarBtn"
- key="sidebar"
- title="Toggle Sidebar"
- style={{
- display: !this._props.isContentActive() ? 'none' : undefined,
- top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5,
- backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
- }}
- onPointerDown={e => this.sidebarBtnDown(e, true)}>
- <FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" />
+ <div className="pdfBox-sidebarBtn-container">
+ <div
+ className="pdfBox-sidebarBtn"
+ key="sidebar"
+ title="Toggle Sidebar"
+ style={{
+ display: !this._props.isContentActive() ? 'none' : undefined,
+ top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5,
+ backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
+ }}>
+ {/* // onPointerDown={e => this.sidebarBtnDown(e, true)} */}
+ <IconButton tooltip="Toggle Annotation Palette" icon={<FontAwesomeIcon style={{ color: Colors.WHITE }} icon="palette" />} onPointerDown={e => this.sidebarBtnDown(e, true)} />
+ </div>
+ <div
+ className="pdfBox-sidebarBtn"
+ key="sidebar"
+ title="Toggle Sidebar"
+ style={{
+ display: !this._props.isContentActive() ? 'none' : undefined,
+ top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5,
+ backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK,
+ }}>
+ <IconButton tooltip="Toggle Sidebar" icon={<FontAwesomeIcon style={{ color: Colors.WHITE }} icon="comment-alt" size="sm" />} onPointerDown={e => this.sidebarBtnDown(e, true)} />
+ </div>
</div>
);
}
diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx
index 2f6824466..df990b0c0 100644
--- a/src/client/views/pdf/AnchorMenu.tsx
+++ b/src/client/views/pdf/AnchorMenu.tsx
@@ -51,6 +51,7 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
public StartDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
public StartCropDrag: (e: PointerEvent, ele: HTMLElement) => void = unimplementedFunction;
public Highlight: (color: string) => Opt<Doc> = (/* color: string */) => undefined;
+ public Tape: (color: string) => Opt<Doc> = (/* color: string */) => undefined;
public GetAnchor: (savedAnnotations: Opt<ObservableMap<number, HTMLDivElement[]>>, addAsAnnotation: boolean) => Opt<Doc> = emptyFunction;
public Delete: () => void = unimplementedFunction;
public PinToPres: () => void = unimplementedFunction;
@@ -172,6 +173,12 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
AnchorMenu.Instance.fadeOut(true);
};
+ @action
+ tapeClicked = () => {
+ this.Tape(this.highlightColor);
+ // AnchorMenu.Instance.fadeOut(true);
+ };
+
@computed get highlighter() {
return (
<Group>
@@ -182,6 +189,13 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> {
colorPicker={this.highlightColor}
color={SettingsManager.userColor}
/>
+ <IconButton
+ tooltip="Click to Add Tape" //
+ icon={<FontAwesomeIcon icon="tape" />}
+ onClick={this.tapeClicked}
+ colorPicker={this.highlightColor}
+ color={SettingsManager.userColor}
+ />
<ColorPicker selectedColor={this.highlightColor} setFinalColor={this.changeHighlightColor} setSelectedColor={this.changeHighlightColor} size={Size.XSMALL} />
</Group>
);
diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx
index db47a84e1..fbe3518ec 100644
--- a/src/client/views/pdf/PDFViewer.tsx
+++ b/src/client/views/pdf/PDFViewer.tsx
@@ -65,6 +65,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
@observable _pageSizes: { width: number; height: number }[] = [];
@observable _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>();
+ @observable _savedTapes = new ObservableMap<number, HTMLDivElement[]>();
@observable _textSelecting = true;
@observable _showWaiting = true;
@observable Index: number = -1;
@@ -581,6 +582,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
return <div className={'pdfViewerDash-text' + (this._props.pointerEvents?.() !== 'none' && this._textSelecting && this._props.isContentActive() ? '-selected' : '')} ref={this._viewer} />;
}
savedAnnotations = () => this._savedAnnotations;
+ savedTapes = () => this._savedTapes;
addDocumentWrapper = (doc: Doc | Doc[]) => this._props.addDocument!(doc);
render() {
TraceMobx();
@@ -614,6 +616,7 @@ export class PDFViewer extends ObservableReactComponent<IViewerProps> {
docView={this._props.pdfBox.DocumentView!}
finishMarquee={this.finishMarquee}
savedAnnotations={this.savedAnnotations}
+ savedTapes={this.savedTapes}
selectionText={this.selectionText}
annotationLayer={this._annotationLayer.current}
marqueeContainer={this._mainCont.current}
diff --git a/src/client/views/smartdraw/AnnotationPalette.scss b/src/client/views/smartdraw/AnnotationPalette.scss
new file mode 100644
index 000000000..9f875f61a
--- /dev/null
+++ b/src/client/views/smartdraw/AnnotationPalette.scss
@@ -0,0 +1,10 @@
+.annotation-palette {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ position: absolute;
+ right: 14px;
+ top: 50px;
+ border-radius: 5px;
+ margin: auto;
+}
diff --git a/src/client/views/smartdraw/AnnotationPalette.tsx b/src/client/views/smartdraw/AnnotationPalette.tsx
new file mode 100644
index 000000000..c8ce9e653
--- /dev/null
+++ b/src/client/views/smartdraw/AnnotationPalette.tsx
@@ -0,0 +1,483 @@
+import { faLaptopHouse } from '@fortawesome/free-solid-svg-icons';
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { Slider, Switch } from '@mui/material';
+import { Button, IconButton } from 'browndash-components';
+import { action, computed, makeObservable, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { AiOutlineSend } from 'react-icons/ai';
+import ReactLoading from 'react-loading';
+import { returnAll, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnOne, returnTrue, returnZero } from '../../../ClientUtils';
+import { ActiveInkWidth, Doc, DocListCast, StrListCast } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
+import { InkData, InkField } from '../../../fields/InkField';
+import { BoolCast, DocCast, ImageCast, NumCast } from '../../../fields/Types';
+import { emptyFunction, unimplementedFunction } from '../../../Utils';
+import { Docs } from '../../documents/Documents';
+import { makeUserTemplateButton } from '../../util/DropConverter';
+import { SettingsManager } from '../../util/SettingsManager';
+import { Transform } from '../../util/Transform';
+import { undoable, undoBatch } from '../../util/UndoManager';
+import { CollectionFreeFormView, MarqueeOptionsMenu, MarqueeView } from '../collections/collectionFreeForm';
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveIsInkMask, DocumentView, DocumentViewInternal } from '../nodes/DocumentView';
+import { FieldView } from '../nodes/FieldView';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { DefaultStyleProvider } from '../StyleProvider';
+import './AnnotationPalette.scss';
+import { DrawingOptions, SmartDrawHandler } from './SmartDrawHandler';
+import { DocumentType } from '../../documents/DocumentTypes';
+import { ImageField } from '../../../fields/URLField';
+import { CollectionCarousel3DView } from '../collections/CollectionCarousel3DView';
+
+@observer
+export class AnnotationPalette extends ObservableReactComponent<{}> {
+ static Instance: AnnotationPalette;
+ @observable private _display: boolean = false;
+ @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 _freeFormCanvas = Docs.Create.FreeformDocument([], {});
+ @observable private _drawingCarousel = Docs.Create.CarouselDocument([], {});
+ @observable private _drawings: Doc[] = [];
+ private _drawing: Doc[] = [];
+ @observable private _opts: DrawingOptions = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
+ private _gptRes: string[] = [];
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ AnnotationPalette.Instance = this;
+ }
+
+ public static LayoutString(fieldKey: string) {
+ return FieldView.LayoutString(AnnotationPalette, fieldKey);
+ }
+
+ public get FreeformCanvas() {
+ return this._freeFormCanvas;
+ }
+
+ public get DrawingCarousel() {
+ return this._drawingCarousel;
+ }
+
+ // componentDidUpdate(prevProps: Readonly<{}>) {
+ // const docView = DocumentView.getDocumentView(this._freeFormCanvas);
+ // const componentView = docView?.ComponentView as CollectionFreeFormView;
+ // if (componentView) {
+ // componentView.fitContentOnce();
+ // }
+ // this._freeFormCanvas._freeform_fitContentsToBox = false;
+ // }
+
+ return170 = () => 170;
+
+ @action
+ handleKeyPress = async (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ // if (this._showRegenerate) {
+ // this.regenerate();
+ // } else {
+ await this.generateDrawing();
+ // }
+ }
+ };
+
+ @action
+ setPaletteMode = (mode: 'create' | 'view') => {
+ this._paletteMode = mode;
+ };
+
+ @action
+ setUserInput = (input: string) => {
+ if (!this._isLoading) this._userInput = input;
+ };
+
+ @action
+ setDetail = (detail: number) => {
+ if (this._canInteract) this._opts.complexity = detail;
+ };
+
+ @action
+ setColor = (autoColor: boolean) => {
+ if (this._canInteract) this._opts.autoColor = autoColor;
+ };
+
+ @action
+ setSize = (size: number) => {
+ if (this._canInteract) this._opts.size = size;
+ };
+
+ @action
+ resetPalette = (changePaletteMode: boolean) => {
+ if (changePaletteMode) this.setPaletteMode('view');
+ this.setUserInput('');
+ this.setDetail(5);
+ this.setColor(true);
+ this.setSize(200);
+ this._freeFormCanvas = Docs.Create.FreeformDocument([], {});
+ this._drawingCarousel = Docs.Create.CarouselDocument([], {});
+ this._showRegenerate = false;
+ this._canInteract = true;
+ this._drawing = [];
+ this._drawings = [];
+ this._opts = { text: '', complexity: 5, size: 200, autoColor: true, x: 0, y: 0 };
+ this._gptRes = [];
+ };
+
+ addToPalette = async (doc: Doc) => {
+ if (!doc.savedAsAnno) {
+ const clone = await Doc.MakeClone(doc);
+ clone.clone.title = doc.title;
+ const image = await this.getIcon(doc);
+ if (image) {
+ const imageDoc = Docs.Create.ImageDocument(image);
+ Doc.AddDocToList(Doc.MyAnnos, 'data', imageDoc);
+ }
+ doc.savedAsAnno = true;
+ // const templateBtn = makeUserTemplateButton(clone.clone);
+ // Doc.AddDocToList(Doc.MyAnnos, 'data', templateBtn);
+ // this.resetPalette(true);
+ }
+ };
+
+ @action
+ displayPalette = (display: boolean) => {
+ this._display = display;
+ };
+
+ @undoBatch
+ generateDrawing = action(async () => {
+ this._isLoading = true;
+ this._drawings = [];
+ this._drawing = [];
+ for (var i = 0; i < 3; i++) {
+ try {
+ SmartDrawHandler.Instance._addFunc = this.createDrawing;
+ this._canInteract = false;
+ if (this._showRegenerate) {
+ SmartDrawHandler.Instance._deleteFunc = unimplementedFunction;
+ await SmartDrawHandler.Instance.regenerate(this._opts, this._gptRes[i], this._userInput);
+ } else {
+ await SmartDrawHandler.Instance.drawWithGPT({ X: 0, Y: 0 }, this._userInput, this._opts.complexity, this._opts.size, this._opts.autoColor);
+ }
+ } catch (e) {
+ console.log('Error generating drawing');
+ }
+ }
+ this._opts.text !== '' ? (this._opts.text = `${this._opts.text} ~~~ ${this._userInput}`) : (this._opts.text = this._userInput);
+ this._userInput = '';
+ this._isLoading = false;
+ this._showRegenerate = true;
+ });
+
+ @action
+ createDrawing = (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string) => {
+ this._opts = opts;
+ this._gptRes.push(gptRes);
+ this._drawing = [];
+ // const childDocs = DocListCast(this._drawing1[DocData].data);
+ strokeList.forEach((stroke: [InkData, string, string]) => {
+ const bounds = InkField.getBounds(stroke[0]);
+ const inkWidth = 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,
+ stroke[1],
+ ActiveInkBezierApprox(),
+ stroke[2] === 'none' ? ActiveFillColor() : stroke[2],
+ ActiveArrowStart(),
+ ActiveArrowEnd(),
+ ActiveDash(),
+ ActiveIsInkMask()
+ );
+ this._drawing.push(inkDoc);
+ // childDocs.push(inkDoc);
+ });
+
+ const cv = DocumentView.getDocumentView(this._freeFormCanvas)?.ComponentView as CollectionFreeFormView;
+ const collection = cv._marqueeViewRef.current?.collection(undefined, true, this._drawing);
+ if (collection) {
+ this._drawings.push(collection);
+ cv.fitContentOnce();
+ }
+ this._drawingCarousel = Docs.Create.CarouselDocument(this._drawings, { childLayoutFitWidth: true, _layout_fitWidth: true, _freeform_fitContentsToBox: true });
+ this._freeFormCanvas = Docs.Create.FreeformDocument(this._drawing, { _freeform_fitContentsToBox: true });
+ };
+
+ saveDrawing = async () => {
+ // const cv = DocumentView.getDocumentView(this._freeFormCanvas)?.ComponentView as CollectionFreeFormView;
+ // if (cv) {
+ // const collection = cv._marqueeViewRef.current?.collection(undefined, true, this._drawing);
+ const cIndex: number = this._drawingCarousel.carousel_index as number;
+ const focusedDrawing = this._drawings[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;
+ // const image = await this.getIcon(collection);
+ await this.addToPalette(focusedDrawing);
+
+ // if (collection) {
+ // const docData = collection[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;
+ // docData.width = this._opts.size;
+ // // const image = await this.getIcon(collection);
+ // await this.addToPalette(collection);
+ // }
+ // }
+ };
+
+ async getIcon(group: Doc) {
+ const docView = DocumentView.getDocumentView(group);
+ if (docView) {
+ docView.ComponentView?.updateIcon?.();
+ return new Promise<ImageField | undefined>(res => setTimeout(() => res(ImageCast(docView.Document.icon)), 1000));
+ }
+ return undefined;
+ }
+
+ @computed get drawingCreator() {
+ return (
+ <DocumentView
+ Document={this._freeFormCanvas}
+ addDocument={undefined}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
+ pinToPres={DocumentView.PinDoc}
+ containerViewPath={returnEmptyDoclist}
+ styleProvider={DefaultStyleProvider}
+ removeDocument={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ PanelWidth={this.return170}
+ PanelHeight={this.return170}
+ renderDepth={1}
+ isContentActive={returnTrue}
+ focus={emptyFunction}
+ whenChildContentsActiveChanged={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ />
+ );
+ }
+
+ render() {
+ return !this._display ? null : (
+ <div className="annotation-palette" style={{ zIndex: 1000 }}>
+ {this._paletteMode === 'view' && (
+ <>
+ <DocumentView
+ Document={Doc.MyAnnos}
+ addDocument={undefined}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
+ pinToPres={DocumentView.PinDoc}
+ containerViewPath={returnEmptyDoclist}
+ styleProvider={DefaultStyleProvider}
+ removeDocument={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ PanelWidth={this.return170}
+ PanelHeight={this.return170}
+ renderDepth={0}
+ isContentActive={returnTrue}
+ focus={emptyFunction}
+ whenChildContentsActiveChanged={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ />
+ <Button
+ // style={{ alignSelf: 'center' }}
+ text="Add"
+ icon={<FontAwesomeIcon icon="square-plus" />}
+ // iconPlacement=""
+ color={SettingsManager.userColor}
+ onClick={() => this.setPaletteMode('create')}
+ />
+ </>
+ )}
+ {this._paletteMode === 'create' && (
+ <>
+ <div style={{ display: 'flex', flexDirection: 'row', width: '170px' }}>
+ {/* <IconButton
+ tooltip="Advanced Options"
+ icon={<FontAwesomeIcon icon="caret-right" />}
+ color={SettingsManager.userColor}
+ style={{ width: '14px' }}
+ // onClick={() => {
+ // this._showOptions = !this._showOptions;
+ // }}
+ /> */}
+ <input
+ aria-label="label-input"
+ id="new-label"
+ type="text"
+ style={{ color: 'black', width: '170px' }}
+ value={this._userInput}
+ onChange={e => {
+ this.setUserInput(e.target.value);
+ }}
+ placeholder={this._showRegenerate ? '(Optional) Enter edits' : 'Enter item to draw'}
+ onKeyDown={this.handleKeyPress}
+ />
+ <Button
+ style={{ alignSelf: 'flex-end' }}
+ tooltip={this._showRegenerate ? 'Regenerate' : 'Send'}
+ icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : this._showRegenerate ? <FontAwesomeIcon icon={'rotate'} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ color={SettingsManager.userColor}
+ onClick={this.generateDrawing}
+ />
+ </div>
+ <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', width: '170px', marginTop: '5px' }}>
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '40px' }}>
+ Color
+ <Switch
+ sx={{
+ '& .MuiSwitch-switchBase.Mui-checked': {
+ color: SettingsManager.userColor,
+ },
+ '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
+ backgroundColor: SettingsManager.userVariantColor,
+ },
+ }}
+ defaultChecked={true}
+ value={this._opts.autoColor}
+ size="small"
+ onChange={() => this.setColor(!this._opts.autoColor)}
+ />
+ </div>
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '60px' }}>
+ Detail
+ <Slider
+ sx={{
+ '& .MuiSlider-thumb': {
+ color: SettingsManager.userColor,
+ '&.Mui-focusVisible, &:hover, &.Mui-active': {
+ boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`,
+ },
+ },
+ '& .MuiSlider-track': {
+ color: SettingsManager.userVariantColor,
+ },
+ '& .MuiSlider-rail': {
+ color: SettingsManager.userColor,
+ },
+ }}
+ style={{ width: '80%' }}
+ min={1}
+ max={10}
+ step={1}
+ size="small"
+ value={this._opts.complexity}
+ onChange={(e, val) => {
+ this.setDetail(val as number);
+ }}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', width: '60px' }}>
+ Size
+ <Slider
+ sx={{
+ '& .MuiSlider-thumb': {
+ color: SettingsManager.userColor,
+ '&.Mui-focusVisible, &:hover, &.Mui-active': {
+ boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`,
+ },
+ },
+ '& .MuiSlider-track': {
+ color: SettingsManager.userVariantColor,
+ },
+ '& .MuiSlider-rail': {
+ color: SettingsManager.userColor,
+ },
+ }}
+ style={{ width: '80%' }}
+ min={50}
+ max={500}
+ step={10}
+ size="small"
+ value={this._opts.size}
+ onChange={(e, val) => {
+ this.setSize(val as number);
+ }}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ </div>
+ <div style={{ display: 'none' }}>
+ <DocumentView
+ Document={this._freeFormCanvas}
+ addDocument={undefined}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
+ pinToPres={DocumentView.PinDoc}
+ containerViewPath={returnEmptyDoclist}
+ styleProvider={DefaultStyleProvider}
+ removeDocument={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ PanelWidth={this.return170}
+ PanelHeight={this.return170}
+ renderDepth={1}
+ isContentActive={returnTrue}
+ focus={emptyFunction}
+ whenChildContentsActiveChanged={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ />
+ </div>
+ <DocumentView
+ Document={this._drawingCarousel}
+ addDocument={undefined}
+ addDocTab={DocumentViewInternal.addDocTabFunc}
+ pinToPres={DocumentView.PinDoc}
+ containerViewPath={returnEmptyDoclist}
+ styleProvider={DefaultStyleProvider}
+ removeDocument={returnFalse}
+ ScreenToLocalTransform={Transform.Identity}
+ PanelWidth={this.return170}
+ PanelHeight={this.return170}
+ renderDepth={1}
+ isContentActive={returnTrue}
+ focus={emptyFunction}
+ whenChildContentsActiveChanged={emptyFunction}
+ childFilters={returnEmptyFilter}
+ childFiltersByRanges={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ />
+ <div style={{ width: '100%', display: 'flex', flexDirection: 'row', justifyContent: 'space-between' }}>
+ <Button text="Back" tooltip="Back to All Annotations" icon={<FontAwesomeIcon icon="reply" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(true)} />
+ <div style={{ display: 'flex', flexDirection: 'row' }}>
+ <Button tooltip="Save" icon={<FontAwesomeIcon icon="file-arrow-down" />} color={SettingsManager.userColor} onClick={this.saveDrawing} />
+ <Button tooltip="Reset" icon={<FontAwesomeIcon icon="rotate-left" />} color={SettingsManager.userColor} onClick={() => this.resetPalette(false)} />
+ </div>
+ </div>
+ </>
+ )}
+ </div>
+ );
+ }
+}
+
+Docs.Prototypes.TemplateMap.set(DocumentType.ANNOPALETTE, {
+ layout: { view: AnnotationPalette, dataField: 'data' },
+ options: { acl: '' },
+});
diff --git a/src/client/views/smartdraw/SmartDrawHandler.scss b/src/client/views/smartdraw/SmartDrawHandler.scss
new file mode 100644
index 000000000..6d402a80f
--- /dev/null
+++ b/src/client/views/smartdraw/SmartDrawHandler.scss
@@ -0,0 +1,3 @@
+.smart-draw-handler {
+ position: absolute;
+}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
new file mode 100644
index 000000000..489f6f92b
--- /dev/null
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -0,0 +1,439 @@
+import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
+import { action, makeObservable, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import React from 'react';
+import { SettingsManager } from '../../util/SettingsManager';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import { Button, IconButton } from 'browndash-components';
+import ReactLoading from 'react-loading';
+import { AiOutlineSend } from 'react-icons/ai';
+import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
+import { InkData, InkTool } from '../../../fields/InkField';
+import { SVGToBezier } from '../../util/bezierFit';
+const { parse } = require('svgson');
+import { Slider, Switch } from '@mui/material';
+import { Doc } from '../../../fields/Doc';
+import { DocData } from '../../../fields/DocSymbols';
+import { DocumentView } from '../nodes/DocumentView';
+import { BoolCast, NumCast, StrCast } from '../../../fields/Types';
+import './SmartDrawHandler.scss';
+import { unimplementedFunction } from '../../../Utils';
+
+export interface DrawingOptions {
+ text: string;
+ complexity: number;
+ size: number;
+ autoColor: boolean;
+ x: number;
+ y: number;
+}
+
+@observer
+export class SmartDrawHandler extends ObservableReactComponent<{}> {
+ static Instance: SmartDrawHandler;
+
+ @observable private _display: boolean = false;
+ @observable private _pageX: number = 0;
+ @observable private _pageY: number = 0;
+ @observable private _yRelativeToTop: boolean = true;
+ @observable private _isLoading: boolean = false;
+ @observable private _userInput: string = '';
+ @observable private _showOptions: boolean = false;
+ @observable private _showEditBox: boolean = false;
+ @observable public _showRegenerate: boolean = false;
+ @observable private _complexity: number = 5;
+ @observable private _size: number = 200;
+ @observable private _autoColor: boolean = true;
+ @observable private _regenInput: string = '';
+ @observable private _canInteract: boolean = true;
+ public _addFunc: (strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void = () => {};
+ public _deleteFunc: (doc?: Doc) => void = () => {};
+ private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
+ private _lastResponse: string = '';
+ private _selectedDoc: Doc | undefined = undefined;
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ SmartDrawHandler.Instance = this;
+ }
+
+ @action
+ setUserInput = (input: string) => {
+ if (this._canInteract) this._userInput = input;
+ };
+
+ @action
+ setRegenInput = (input: string) => {
+ if (this._canInteract) this._regenInput = input;
+ };
+
+ @action
+ setShowOptions = () => {
+ this._showOptions = !this._showOptions;
+ };
+
+ @action
+ setComplexity = (val: number) => {
+ if (this._canInteract) this._complexity = val;
+ };
+
+ @action
+ setSize = (val: number) => {
+ if (this._canInteract) this._size = val;
+ };
+
+ @action
+ setAutoColor = () => {
+ if (this._canInteract) this._autoColor = !this._autoColor;
+ };
+
+ @action
+ displaySmartDrawHandler = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
+ this._pageX = x;
+ this._pageY = y;
+ this._display = true;
+ this._addFunc = addFunc;
+ this._deleteFunc = deleteFunc;
+ };
+
+ @action
+ displayRegenerate = (x: number, y: number, addFunc: (strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void, deleteFunc: (doc?: Doc) => void) => {
+ this._selectedDoc = DocumentView.SelectedDocs().lastElement();
+ const docData = this._selectedDoc[DocData];
+ this._addFunc = addFunc;
+ this._deleteFunc = deleteFunc;
+ this._pageX = x;
+ this._pageY = y;
+ this._display = false;
+ this._showRegenerate = true;
+ this._showEditBox = false;
+ this._lastResponse = StrCast(docData.drawingData);
+ this._lastInput = { text: StrCast(docData.drawingInput), complexity: NumCast(docData.drawingComplexity), size: NumCast(docData.drawingSize), autoColor: BoolCast(docData.drawingColored), x: this._pageX, y: this._pageY };
+ };
+
+ @action
+ hideSmartDrawHandler = () => {
+ this._showRegenerate = false;
+ this._display = false;
+ this._isLoading = false;
+ this._showOptions = false;
+ this._userInput = '';
+ this._complexity = 5;
+ this._size = 350;
+ this._autoColor = true;
+ Doc.ActiveTool = InkTool.None;
+ this._lastInput = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
+ };
+
+ @action
+ hideRegenerate = () => {
+ if (!this._isLoading) {
+ this._showRegenerate = false;
+ this._isLoading = false;
+ this._regenInput = '';
+ this._lastInput = { text: '', complexity: 5, size: 350, autoColor: true, x: 0, y: 0 };
+ }
+ };
+
+ @action
+ handleKeyPress = async (event: React.KeyboardEvent) => {
+ if (event.key === 'Enter') {
+ await this.handleSendClick();
+ }
+ };
+
+ @action
+ handleSendClick = async () => {
+ this._isLoading = true;
+ this._canInteract = false;
+ if (this._showRegenerate) {
+ await this.regenerate();
+ this._regenInput = '';
+ this._showEditBox = false;
+ } else {
+ this._showOptions = false;
+ await this.drawWithGPT({ X: this._pageX, Y: this._pageY }, this._userInput, this._complexity, this._size, this._autoColor);
+ this.hideSmartDrawHandler();
+ this._showRegenerate = true;
+ }
+ this._isLoading = false;
+ this._canInteract = true;
+ };
+
+ _errorOccurredOnce = false;
+ @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 };
+
+ try {
+ const res = await gptAPICall(`"${input}", "${complexity}", "${size}"`, GPTCallType.DRAW, undefined, true);
+ if (!res) {
+ console.error('GPT call failed');
+ return;
+ }
+ console.log(res);
+ const strokeData = await this.parseResponse(res, startPt, false, autoColor);
+ this._errorOccurredOnce = false;
+ return strokeData;
+ } catch (err) {
+ if (this._errorOccurredOnce) {
+ console.error('GPT call failed', err);
+ this._errorOccurredOnce = false;
+ } else {
+ this._errorOccurredOnce = true;
+ this.drawWithGPT(startPt, input, complexity, size, autoColor);
+ }
+ }
+ };
+
+ @action
+ edit = () => {
+ this._showEditBox = !this._showEditBox;
+ };
+
+ @action
+ regenerate = async (lastInput?: DrawingOptions, lastResponse?: string, regenInput?: string) => {
+ if (lastInput) this._lastInput = lastInput;
+ if (lastResponse) this._lastResponse = lastResponse;
+ if (regenInput) this._regenInput = regenInput;
+
+ try {
+ let res;
+ if (this._regenInput !== '') {
+ const prompt: string = `This is your previously generated svg code: ${this._lastResponse} for the user input "${this._lastInput.text}". Please regenerate it with the provided specifications.`;
+ res = await gptAPICall(`"${this._regenInput}"`, GPTCallType.DRAW, prompt, true);
+ this._lastInput.text = `${this._lastInput.text} ~~~ ${this._regenInput}`;
+ } else {
+ res = await gptAPICall(`"${this._lastInput.text}", "${this._lastInput.complexity}", "${this._lastInput.size}"`, GPTCallType.DRAW, undefined, true);
+ }
+ if (!res) {
+ console.error('GPT call failed');
+ return;
+ }
+ console.log(res);
+ this.parseResponse(res, { X: this._lastInput.x, Y: this._lastInput.y }, true, lastInput?.autoColor || this._autoColor);
+ } catch (err) {
+ console.error('GPT call failed', err);
+ }
+ };
+
+ @action
+ parseResponse = async (res: string, startPoint: { X: number; Y: number }, regenerate: boolean, autoColor: boolean) => {
+ const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
+ if (svg) {
+ this._lastResponse = svg[0];
+ const svgObject = await parse(svg[0]);
+ const svgStrokes: any = svgObject.children;
+ const strokeData: [InkData, string, string][] = [];
+ svgStrokes.forEach((child: any) => {
+ const convertedBezier: InkData = SVGToBezier(child.name, child.attributes);
+ strokeData.push([
+ convertedBezier.map(point => {
+ return { X: point.X + startPoint.X - this._size / 1.5, Y: point.Y + startPoint.Y - this._size / 2 };
+ }),
+ (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.stroke : undefined,
+ (regenerate ? this._lastInput.autoColor : autoColor) ? child.attributes.fill : undefined,
+ ]);
+ });
+ if (regenerate) {
+ if (this._deleteFunc !== unimplementedFunction) this._deleteFunc(this._selectedDoc);
+ this._addFunc(strokeData, this._lastInput, svg[0], this._selectedDoc);
+ } else {
+ this._addFunc(strokeData, this._lastInput, svg[0]);
+ }
+ return { data: strokeData, lastInput: this._lastInput, lastRes: svg[0] };
+ }
+ };
+
+ render() {
+ if (this._display) {
+ return (
+ <div
+ id="label-handler"
+ className="smart-draw-handler"
+ style={{
+ display: this._display ? '' : 'none',
+ left: this._pageX,
+ ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
+ background: SettingsManager.userBackgroundColor,
+ color: SettingsManager.userColor,
+ }}>
+ <div>
+ <IconButton
+ tooltip={'Cancel'}
+ onClick={() => {
+ this.hideSmartDrawHandler();
+ this.hideRegenerate();
+ }}
+ icon={<FontAwesomeIcon icon="xmark" />}
+ color={SettingsManager.userColor}
+ style={{ width: '19px' }}
+ />
+ <input
+ aria-label="Smart Draw Input"
+ className="smartdraw-input"
+ id="smartdraw-input"
+ type="text"
+ style={{ color: 'black' }}
+ value={this._userInput}
+ onChange={e => {
+ this.setUserInput(e.target.value);
+ }}
+ placeholder="Enter item to draw"
+ onKeyDown={this.handleKeyPress}
+ />
+ <IconButton tooltip="Advanced Options" icon={<FontAwesomeIcon icon={this._showOptions ? 'caret-down' : 'caret-right'} />} color={SettingsManager.userColor} style={{ width: '14px' }} onClick={this.setShowOptions} />
+ <Button
+ style={{ alignSelf: 'flex-end' }}
+ text="Send"
+ icon={this._isLoading ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ color={SettingsManager.userColor}
+ onClick={this.handleSendClick}
+ />
+ </div>
+ {this._showOptions && (
+ <>
+ <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-around' }}>
+ <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '30%' }}>
+ Auto color
+ <Switch
+ sx={{
+ '& .MuiSwitch-switchBase.Mui-checked': {
+ color: SettingsManager.userColor,
+ },
+ '& .MuiSwitch-switchBase.Mui-checked + .MuiSwitch-track': {
+ backgroundColor: SettingsManager.userVariantColor,
+ },
+ }}
+ defaultChecked={true}
+ value={this._autoColor}
+ size="small"
+ onChange={this.setAutoColor}
+ />
+ </div>
+ <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '31%' }}>
+ Complexity
+ <Slider
+ sx={{
+ '& .MuiSlider-thumb': {
+ color: SettingsManager.userColor,
+ '&.Mui-focusVisible, &:hover, &.Mui-active': {
+ boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}10`,
+ },
+ },
+ '& .MuiSlider-track': {
+ color: SettingsManager.userVariantColor,
+ },
+ '& .MuiSlider-rail': {
+ color: SettingsManager.userColor,
+ },
+ }}
+ style={{ width: '80%' }}
+ min={1}
+ max={10}
+ step={1}
+ size="small"
+ value={this._complexity}
+ onChange={(e, val) => {
+ this.setComplexity(val as number);
+ }}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ <div style={{ display: 'flex', flexDirection: 'column', justifyContent: 'center', width: '39%' }}>
+ Size (in pixels)
+ <Slider
+ sx={{
+ '& .MuiSlider-thumb': {
+ color: SettingsManager.userColor,
+ '&.Mui-focusVisible, &:hover, &.Mui-active': {
+ boxShadow: `0px 0px 0px 8px${SettingsManager.userColor.slice(0, 7)}20`,
+ },
+ },
+ '& .MuiSlider-track': {
+ color: SettingsManager.userVariantColor,
+ },
+ '& .MuiSlider-rail': {
+ color: SettingsManager.userColor,
+ },
+ }}
+ style={{ width: '80%' }}
+ min={50}
+ max={700}
+ step={10}
+ size="small"
+ value={this._size}
+ onChange={(e, val) => {
+ this.setSize(val as number);
+ }}
+ valueLabelDisplay="auto"
+ />
+ </div>
+ </div>
+ </>
+ )}
+ </div>
+ );
+ } else if (this._showRegenerate) {
+ return (
+ <div
+ id="smartdraw-options-menu"
+ className="smart-draw-handler"
+ style={{
+ left: this._pageX,
+ ...(this._yRelativeToTop ? { top: Math.max(0, this._pageY) } : { bottom: this._pageY }),
+ background: SettingsManager.userBackgroundColor,
+ color: SettingsManager.userColor,
+ }}>
+ <div
+ style={{
+ display: 'flex',
+ flexDirection: 'row',
+ }}>
+ <IconButton
+ tooltip="Regenerate"
+ icon={this._isLoading && this._regenInput === '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <FontAwesomeIcon icon={'rotate'} />}
+ color={SettingsManager.userColor}
+ onClick={this.handleSendClick}
+ />
+ <IconButton tooltip="Edit with GPT" icon={<FontAwesomeIcon icon="pen-to-square" />} color={SettingsManager.userColor} onClick={this.edit} />
+ {this._showEditBox && (
+ <div
+ style={{
+ display: 'flex',
+ flexDirection: 'row',
+ }}>
+ <input
+ aria-label="Edit instructions input"
+ className="smartdraw-input"
+ id="regen-input"
+ type="text"
+ style={{ color: 'black' }}
+ value={this._regenInput}
+ onChange={e => {
+ this.setRegenInput(e.target.value);
+ }}
+ onKeyDown={this.handleKeyPress}
+ placeholder="Edit instructions"
+ />
+ <Button
+ style={{ alignSelf: 'flex-end' }}
+ text="Send"
+ icon={this._isLoading && this._regenInput !== '' ? <ReactLoading type="spin" color={SettingsManager.userVariantColor} width={16} height={20} /> : <AiOutlineSend />}
+ iconPlacement="right"
+ color={SettingsManager.userColor}
+ onClick={this.handleSendClick}
+ />
+ </div>
+ )}
+ </div>
+ </div>
+ );
+ } else {
+ return <></>;
+ }
+ }
+}
diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts
index 72ec16b42..c86342870 100644
--- a/src/fields/Doc.ts
+++ b/src/fields/Doc.ts
@@ -174,6 +174,53 @@ export function updateCachedAcls(doc: Doc) {
return undefined;
}
+export function ActiveInkPen(): Doc { return Doc.UserDoc(); } // prettier-ignore
+export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, 'black'); } // prettier-ignore
+export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, ''); } // prettier-ignore
+export function ActiveIsInkMask(): boolean { return BoolCast(ActiveInkPen()?.activeIsInkMask, false); } // prettier-ignore
+export function ActiveInkHideTextLabels(): boolean { return BoolCast(ActiveInkPen().activeInkHideTextLabels, false); } // prettier-ignore
+export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ''); } // prettier-ignore
+export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ''); } // prettier-ignore
+export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); } // prettier-ignore
+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 NumCast(ActiveInkPen()?.eraserWidth); } // prettier-ignore
+
+export function SetActiveInkWidth(width: string): void {
+ !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width);
+}
+export function SetActiveBezierApprox(bezier: string): void {
+ ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? '' : bezier);
+}
+export function SetActiveInkColor(value: string) {
+ ActiveInkPen() && (ActiveInkPen().activeInkColor = value);
+}
+export function SetActiveIsInkMask(value: boolean) {
+ ActiveInkPen() && (ActiveInkPen().activeIsInkMask = value);
+}
+export function SetActiveInkHideTextLabels(value: boolean) {
+ ActiveInkPen() && (ActiveInkPen().activeInkHideTextLabels = value);
+}
+export function SetActiveFillColor(value: string) {
+ ActiveInkPen() && (ActiveInkPen().activeFillColor = value);
+}
+export function SetActiveArrowStart(value: string) {
+ ActiveInkPen() && (ActiveInkPen().activeArrowStart = value);
+}
+export function SetActiveArrowEnd(value: string) {
+ ActiveInkPen() && (ActiveInkPen().activeArrowEnd = value);
+}
+export function SetActiveArrowScale(value: number) {
+ ActiveInkPen() && (ActiveInkPen().activeArrowScale = value);
+}
+export function SetActiveDash(dash: string): void {
+ !isNaN(parseInt(dash)) && ActiveInkPen() && (ActiveInkPen().activeDash = dash);
+}
+export function SetEraserWidth(width: number): void {
+ !isNaN(width) && ActiveInkPen() && (ActiveInkPen().eraserWidth = width);
+}
+
@scriptingGlobal
@Deserializable('Doc', updateCachedAcls, ['id'])
export class Doc extends RefField {
@@ -229,6 +276,7 @@ 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 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
@@ -1693,8 +1741,8 @@ ScriptingGlobals.add(function getEmbedding(doc: any) {
return Doc.MakeEmbedding(doc);
});
// eslint-disable-next-line prefer-arrow-callback
-ScriptingGlobals.add(function getCopy(doc: any, copyProto: any) {
- return doc.isTemplateDoc ? Doc.MakeDelegateWithProto(doc) : Doc.MakeCopy(doc, copyProto);
+ScriptingGlobals.add(async function getCopy(doc: any, copyProto: any) {
+ return doc.isTemplateDoc ? (doc.isGroup ? (await Doc.MakeClone(doc)).clone : Doc.MakeDelegateWithProto(doc)) : Doc.MakeCopy(doc, copyProto);
});
// eslint-disable-next-line prefer-arrow-callback
ScriptingGlobals.add(function copyField(field: any) {
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index 32abf0076..123d32301 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -17,6 +17,7 @@ export enum InkTool {
Stamp = 'stamp',
Write = 'write',
PresentationPin = 'presentationpin',
+ SmartDraw = 'smartdraw',
}
export type Segment = Array<Bezier>;