aboutsummaryrefslogtreecommitdiff
path: root/src/client/views
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views')
-rw-r--r--src/client/views/GestureOverlay.tsx28
-rw-r--r--src/client/views/LightboxView.scss24
-rw-r--r--src/client/views/LightboxView.tsx7
-rw-r--r--src/client/views/MainView.tsx5
-rw-r--r--src/client/views/MarqueeAnnotator.tsx146
-rw-r--r--src/client/views/collections/CollectionMenu.scss2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx242
-rw-r--r--src/client/views/collections/collectionFreeForm/ImageLabelHandler.scss14
-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/DrawingPalette.scss11
-rw-r--r--src/client/views/smartdraw/DrawingPalette.tsx89
-rw-r--r--src/client/views/smartdraw/SmartDrawHandler.tsx409
17 files changed, 950 insertions, 107 deletions
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index e3e252593..a1f7712c1 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -240,15 +240,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 });
@@ -291,13 +291,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/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..e93e4949b 100644
--- a/src/client/views/LightboxView.tsx
+++ b/src/client/views/LightboxView.tsx
@@ -23,6 +23,7 @@ import { DocumentView } from './nodes/DocumentView';
import { OpenWhere, OpenWhereMod } from './nodes/OpenWhere';
import { ScriptingGlobals } from '../util/ScriptingGlobals';
import { OverlayView } from './OverlayView';
+import { DrawingPalette } from './smartdraw/DrawingPalette';
interface LightboxViewProps {
PanelWidth: number;
@@ -59,6 +60,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
@@ -202,6 +204,9 @@ export class LightboxView extends ObservableReactComponent<LightboxViewProps> {
toggleFitWidth = () => {
this._doc && (this._doc._layout_fitWidth = !this._doc._layout_fitWidth);
};
+ togglePalette = () => {
+ this._showPalette = !this._showPalette;
+ };
togglePen = () => {
Doc.ActiveTool = Doc.ActiveTool === InkTool.Pen ? InkTool.None : InkTool.Pen;
};
@@ -319,8 +324,10 @@ 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-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)}
+ {this._showPalette && <DrawingPalette />}
</div>
);
}
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index ef1bcfb64..30720a3a2 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -76,6 +76,7 @@ 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';
const { LEFT_MENU_WIDTH, TOPBAR_HEIGHT } = require('./global/globalCssVariables.module.scss'); // prettier-ignore
const _global = (window /* browser */ || global) /* node */ as any;
@@ -317,6 +318,7 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faCompass,
fa.faSnowflake,
fa.faStar,
+ fa.faSplotch,
fa.faMicrophone,
fa.faCircleHalfStroke,
fa.faKeyboard,
@@ -394,6 +396,7 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faArrowsLeftRight,
fa.faPause,
fa.faPen,
+ fa.faUserPen,
fa.faPenNib,
fa.faPhone,
fa.faPlay,
@@ -439,6 +442,7 @@ export class MainView extends ObservableReactComponent<{}> {
fa.faEyeDropper,
fa.faPaintRoller,
fa.faBars,
+ fa.faBarsStaggered,
fa.faBrush,
fa.faShapes,
fa.faEllipsisH,
@@ -1089,6 +1093,7 @@ export class MainView extends ObservableReactComponent<{}> {
<TaskCompletionBox />
<ContextMenu />
<ImageLabelHandler />
+ <SmartDrawHandler />
<AnchorMenu />
<MapAnchorMenu />
<DirectionsAnchorMenu />
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/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..8f8cb9083 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,7 @@ import { CollectionFreeFormPannableContents } from './CollectionFreeFormPannable
import { CollectionFreeFormRemoteCursors } from './CollectionFreeFormRemoteCursors';
import './CollectionFreeFormView.scss';
import { MarqueeView } from './MarqueeView';
+import { DrawingOptions, SmartDrawHandler } from '../../smartdraw/SmartDrawHandler';
@observer
class CollectionFreeFormOverlayView extends React.Component<{ elements: () => ViewDefResult[] }> {
@@ -118,6 +120,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 +500,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 +574,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}
}
};
+
@action
onEraserUp = (): void => {
this._deleteList.lastElement()?._props.removeDocument?.(this._deleteList.map(ink => ink.Document));
@@ -609,34 +615,48 @@ 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,
@@ -655,14 +675,30 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
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
@@ -672,32 +708,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 +764,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 +1275,60 @@ 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[] = [];
+ @undoBatch
+ createDrawing = (e: React.PointerEvent<Element>, strokeData: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => {
+ strokeData.forEach((stroke: [InkData, string, string]) => {
+ const bounds = InkField.getBounds(stroke[0]);
+ const B = this.screenToFreeformContentsXf.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],
+ undefined,
+ stroke[2] === 'none' ? undefined : stroke[2]
+ );
+ this._drawing.push(inkDoc);
+ this.addDocument(inkDoc);
+ });
+ const collection = this._marqueeViewRef.current?.collection(undefined, true, this._drawing);
+ if (collection) {
+ const docData = collection[DocData];
+ docData.title = opts.text;
+ docData.drawingInput = opts.text;
+ docData.drawingComplexity = opts.complexity;
+ docData.drawingColored = opts.autoColor;
+ docData.drawingSize = opts.size;
+ docData.drawingData = gptRes;
+ }
+ this._batch?.end();
+ };
+
+ removeDrawing = (doc?: Doc) => {
+ this._batch = UndoManager.StartBatch('regenerateDrawing');
+ if (doc) {
+ const docData: Doc = doc[DocData];
+ const children = docData.data as unknown as Doc[];
+ this._props.removeDocument?.(doc);
+ this._props.removeDocument?.(children);
+ } else {
+ this._props.removeDocument?.(this._drawing);
+ }
+ this._drawing = [];
+ };
+
+ @action
zoom = (pointX: number, pointY: number, deltaY: number): void => {
if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return;
let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05;
@@ -1831,8 +1921,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 +2031,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection
}),
icon: 'eye',
});
+ optionItems.push({
+ description: (this._showDrawingEditor ? 'Close' : 'Show') + ' Drawing Editor',
+ event: action(() => {
+ this._showDrawingEditor = !this._showDrawingEditor;
+ this._showDrawingEditor ? 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 +2243,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/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 2b7de5082..d46e72d75 100644
--- a/src/client/views/global/globalScripts.ts
+++ b/src/client/views/global/globalScripts.ts
@@ -506,9 +506,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/DrawingPalette.scss b/src/client/views/smartdraw/DrawingPalette.scss
new file mode 100644
index 000000000..0f1152b71
--- /dev/null
+++ b/src/client/views/smartdraw/DrawingPalette.scss
@@ -0,0 +1,11 @@
+.drawing-palette {
+ display: grid;
+ grid-template-columns: auto;
+ position: absolute;
+ right: 14px;
+ width: 170px;
+ height: 170px;
+ top: 50px;
+ border-radius: 5px;
+ background-color: white;
+}
diff --git a/src/client/views/smartdraw/DrawingPalette.tsx b/src/client/views/smartdraw/DrawingPalette.tsx
new file mode 100644
index 000000000..87a39bc85
--- /dev/null
+++ b/src/client/views/smartdraw/DrawingPalette.tsx
@@ -0,0 +1,89 @@
+import { computed, makeObservable, observable } from 'mobx';
+import { observer } from 'mobx-react';
+import * as React from 'react';
+import { returnAll, returnFalse, returnOne, returnZero } from '../../../ClientUtils';
+import { Doc, StrListCast } from '../../../fields/Doc';
+import { emptyFunction } from '../../../Utils';
+import { CollectionViewType } from '../../documents/DocumentTypes';
+import { MarqueeView } from '../collections/collectionFreeForm';
+import { CollectionGridView } from '../collections/collectionGrid';
+import { CollectionStackingView } from '../collections/CollectionStackingView';
+import { DocumentView } from '../nodes/DocumentView';
+import { FieldViewProps } from '../nodes/FieldView';
+import { ObservableReactComponent } from '../ObservableReactComponent';
+import './DrawingPalette.scss';
+
+@observer
+export class DrawingPalette extends ObservableReactComponent<{}> {
+ @observable private _savedDrawings: Doc[] = [];
+ @observable _marqueeViewRef = React.createRef<MarqueeView>();
+ private _stackRef = React.createRef<CollectionStackingView>();
+
+ constructor(props: any) {
+ super(props);
+ makeObservable(this);
+ }
+
+ panelWidth = () => 100;
+ panelHeight = () => 100;
+
+ getCollection = () => {
+ return this._marqueeViewRef.current?.collection(undefined, false, this._savedDrawings) || new Doc();
+ };
+
+ @computed get savedDrawingAnnos() {
+ // const savedAnnos = Doc.MyDrawingAnnos;
+ return (
+ <div className="collectionMenu-contMenuButtons" style={{ height: '100%' }}>
+ {/* <DocumentView PanelHeight={this.panelWidth} PanelWidth={this.panelHeight} Document={savedAnnos} renderDepth={2} isContentActive={returnFalse} childFilters={this.childFilters} /> */}
+ {/* <CollectionStackingView
+ {...this._props}
+ Document={savedAnnos}
+ // setContentViewBox={emptyFunction}
+ // NativeWidth={returnZero}
+ // NativeHeight={returnZero}
+ ref={this._stackRef}
+ PanelHeight={this.panelWidth}
+ PanelWidth={this.panelHeight}
+ // childFilters={this.childFilters}
+ // sortFunc={this.sortByLinkAnchorY}
+ // setHeight={this.setHeightCallback}
+ // isAnnotationOverlay={false}
+ // select={emptyFunction}
+ NativeDimScaling={returnOne}
+ // childlayout_showTitle={this.layout_showTitle}
+ isContentActive={returnFalse}
+ isSelected={returnFalse}
+ isAnyChildContentActive={returnFalse}
+ // childDocumentsActive={this._props.isContentActive}
+ whenChildContentsActiveChanged={this._props.whenChildContentsActiveChanged}
+ childHideDecorationTitle
+ // ScreenToLocalTransform={this.screenToLocalTransform}
+ renderDepth={this._props.renderDepth + 1}
+ type_collection={CollectionViewType.Stacking}
+ // fieldKey={'drawing-palette'}
+ pointerEvents={returnAll}
+ /> */}
+ </div>
+ );
+ }
+
+ render() {
+ return (
+ <div className="drawing-palette">
+ {/* {this._savedDrawings.map(doc => {
+ return <DocumentView
+ Document={doc}
+ renderDepth={0}
+ PanelWidth={this.panelWidth}
+ PanelHeight={this.panelHeight}
+ isContentActive={this.isContentActive} />;
+ })} */}
+ {/* <CollectionGridView {...this._props} /> */}
+ {}
+ {/* <DocumentView Document={this.getCollection()} /> */}
+ {this.savedDrawingAnnos}
+ </div>
+ );
+ }
+}
diff --git a/src/client/views/smartdraw/SmartDrawHandler.tsx b/src/client/views/smartdraw/SmartDrawHandler.tsx
new file mode 100644
index 000000000..d24cc9d50
--- /dev/null
+++ b/src/client/views/smartdraw/SmartDrawHandler.tsx
@@ -0,0 +1,409 @@
+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 './ImageLabelHandler.scss';
+import { gptAPICall, GPTCallType } from '../../apis/gpt/GPT';
+import { InkData } 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';
+
+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 private _showRegenerate: boolean = false;
+ @observable private _complexity: number = 5;
+ @observable private _size: number = 200;
+ @observable private _autoColor: boolean = true;
+ @observable private _regenInput: string = '';
+ private _addFunc: (e: React.PointerEvent<Element>, strokeList: [InkData, string, string][], opts: DrawingOptions, gptRes: string, containerDoc?: Doc) => void = () => {};
+ private _deleteFunc: (doc?: Doc) => void = () => {};
+ private _lastInput: DrawingOptions = { text: '', complexity: 5, size: 300, 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) => {
+ this._userInput = input;
+ };
+
+ @action
+ setRegenInput = (input: string) => {
+ this._regenInput = input;
+ };
+
+ @action
+ setShowOptions = () => {
+ this._showOptions = !this._showOptions;
+ };
+
+ @action
+ setComplexity = (val: number) => {
+ this._complexity = val;
+ };
+
+ @action
+ setSize = (val: number) => {
+ this._size = val;
+ };
+
+ @action
+ setAutoColor = () => {
+ this._autoColor = !this._autoColor;
+ };
+
+ @action
+ displaySmartDrawHandler = (x: number, y: number, addFunc: (e: React.PointerEvent<Element>, 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: (e: React.PointerEvent<Element>, 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._showRegenerate = true;
+ this._lastResponse = docData.drawingData as string;
+ this._lastInput = { text: docData.drawingInput as string, complexity: docData.drawingComplexity as number, size: docData.drawingSize as number, autoColor: docData.drawingColored as boolean, 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 = 300;
+ this._autoColor = true;
+ // this._regenInput = ''
+ };
+
+ @action
+ hideRegenerate = () => {
+ this._showRegenerate = false;
+ this._isLoading = false;
+ this._regenInput = '';
+ };
+
+ _errorOccurredOnce = false;
+ @action
+ drawWithGPT = async (e: React.PointerEvent<Element>, input: string) => {
+ if (input === '') return;
+ this._lastInput = { text: input, complexity: this._complexity, size: this._size, autoColor: this._autoColor, x: e.clientX, y: e.clientY };
+ this._isLoading = true;
+ this._showOptions = false;
+ try {
+ const res = await gptAPICall(`"${input}", "${this._complexity}", "${this._size}"`, GPTCallType.DRAW, undefined, true);
+ if (!res) {
+ console.error('GPT call failed');
+ return;
+ }
+ console.log(res);
+ await this.parseResponse(e, res, { X: e.clientX, Y: e.clientY }, false);
+ this.hideSmartDrawHandler();
+ this._showRegenerate = true;
+ this._errorOccurredOnce = false;
+ } catch (err) {
+ if (this._errorOccurredOnce) {
+ console.error('GPT call failed', err);
+ this._errorOccurredOnce = false;
+ } else {
+ this._errorOccurredOnce = true;
+ this.drawWithGPT(e, input);
+ }
+ }
+ this._isLoading = false;
+ };
+
+ @action
+ edit = () => {
+ this._showEditBox = !this._showEditBox;
+ };
+
+ @action
+ regenerate = async (e: React.PointerEvent<Element>) => {
+ this._isLoading = true;
+ 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(e, res, { X: this._lastInput.x, Y: this._lastInput.y }, true);
+ } catch (err) {
+ console.error('GPT call failed', err);
+ }
+ this._isLoading = false;
+ this._regenInput = '';
+ this._showEditBox = false;
+ };
+
+ @action
+ parseResponse = async (e: React.PointerEvent<Element>, res: string, startPoint: { X: number; Y: number }, regenerate: boolean) => {
+ const svg = res.match(/<svg[^>]*>([\s\S]*?)<\/svg>/g);
+ console.log('start point is', startPoint);
+ if (svg) {
+ this._lastResponse = svg[0];
+ const svgObject = await parse(svg[0]);
+ const svgStrokes: any = svgObject.children;
+ const strokeData: [InkData, string, string][] = [];
+ console.log('autocolor is', this._autoColor);
+ 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 : this._autoColor) ? child.attributes.stroke : undefined,
+ (regenerate ? this._lastInput.autoColor : this._autoColor) ? child.attributes.fill : undefined,
+ ]);
+ });
+ if (regenerate) {
+ this._deleteFunc(this._selectedDoc);
+ this._addFunc(e, strokeData, this._lastInput, svg[0], this._selectedDoc);
+ } else {
+ this._addFunc(e, strokeData, this._lastInput, svg[0]);
+ }
+ }
+ };
+
+ render() {
+ if (this._display) {
+ return (
+ <div
+ id="label-handler"
+ className="contextMenu-cont"
+ 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="label-input"
+ id="new-label"
+ type="text"
+ style={{ color: 'black' }}
+ value={this._userInput}
+ onChange={e => {
+ this.setUserInput(e.target.value);
+ }}
+ placeholder="Enter item to draw"
+ />
+ <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={e => {
+ this.drawWithGPT(e as React.PointerEvent<Element>, this._userInput);
+ }}
+ />
+ </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}
+ 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="contextMenu-cont"
+ 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={e => {
+ this.regenerate(e as React.PointerEvent<Element>);
+ }}
+ />
+ <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"
+ id="regen-input"
+ type="text"
+ style={{ color: 'black' }}
+ value={this._regenInput}
+ onChange={e => {
+ this.setRegenInput(e.target.value);
+ }}
+ 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={e => {
+ this.regenerate(e as React.PointerEvent<Element>);
+ }}
+ />
+ </div>
+ )}
+ </div>
+ </div>
+ );
+ } else {
+ return <></>;
+ }
+ }
+}