aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGeireann Lindfield Roberts <60007097+geireann@users.noreply.github.com>2021-12-02 14:39:40 -0500
committerGeireann Lindfield Roberts <60007097+geireann@users.noreply.github.com>2021-12-02 14:39:40 -0500
commit2cb18e75aa487ff98086e15fef93e2f549c30496 (patch)
treeaf24d05b564ca4e0011aee368a38eaedb7981d64
parent2e6709216795e86c8b414dcb2dd45855cf23ea24 (diff)
parentc2cd77ca1d2a67539f0af2a68c1e7336b3bc232b (diff)
Merge branch 'master' into trails-aubrey
-rw-r--r--src/Utils.ts14
-rw-r--r--src/client/documents/Documents.ts4
-rw-r--r--src/client/util/CurrentUserUtils.ts21
-rw-r--r--src/client/util/DragManager.ts2
-rw-r--r--src/client/util/InteractionUtils.tsx18
-rw-r--r--src/client/util/bezierFit.ts1431
-rw-r--r--src/client/views/DocComponent.tsx18
-rw-r--r--src/client/views/DocumentDecorations.scss6
-rw-r--r--src/client/views/DocumentDecorations.tsx61
-rw-r--r--src/client/views/GestureOverlay.tsx6
-rw-r--r--src/client/views/GlobalKeyHandler.ts6
-rw-r--r--src/client/views/InkControlPtHandles.tsx99
-rw-r--r--src/client/views/InkStroke.scss34
-rw-r--r--src/client/views/InkStrokeProperties.ts135
-rw-r--r--src/client/views/InkTangentHandles.tsx28
-rw-r--r--src/client/views/InkingStroke.tsx233
-rw-r--r--src/client/views/MainView.tsx7
-rw-r--r--src/client/views/OverlayView.tsx2
-rw-r--r--src/client/views/Palette.tsx2
-rw-r--r--src/client/views/PropertiesView.tsx38
-rw-r--r--src/client/views/StyleProvider.tsx4
-rw-r--r--src/client/views/collections/CollectionStackingView.tsx2
-rw-r--r--src/client/views/collections/CollectionSubView.tsx7
-rw-r--r--src/client/views/collections/CollectionTreeView.scss9
-rw-r--r--src/client/views/collections/CollectionTreeView.tsx244
-rw-r--r--src/client/views/collections/CollectionView.tsx12
-rw-r--r--src/client/views/collections/TabDocView.tsx3
-rw-r--r--src/client/views/collections/TreeView.scss7
-rw-r--r--src/client/views/collections/TreeView.tsx70
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx34
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx232
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx4
-rw-r--r--src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx4
-rw-r--r--src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx2
-rw-r--r--src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx2
-rw-r--r--src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx2
-rw-r--r--src/client/views/collections/collectionSchema/SchemaTable.tsx2
-rw-r--r--src/client/views/nodes/DocumentView.scss2
-rw-r--r--src/client/views/nodes/DocumentView.tsx11
-rw-r--r--src/client/views/nodes/FieldView.tsx2
-rw-r--r--src/client/views/nodes/FilterBox.tsx2
-rw-r--r--src/client/views/nodes/LinkBox.tsx2
-rw-r--r--src/client/views/nodes/LinkDocPreview.tsx2
-rw-r--r--src/client/views/nodes/ScreenshotBox.tsx2
-rw-r--r--src/client/views/nodes/VideoBox.scss10
-rw-r--r--src/client/views/nodes/VideoBox.tsx11
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx2
-rw-r--r--src/client/views/nodes/formattedText/FormattedTextBox.tsx36
-rw-r--r--src/client/views/search/SearchBox.tsx142
-rw-r--r--src/fields/InkField.ts11
-rw-r--r--src/mobile/AudioUpload.tsx2
-rw-r--r--src/mobile/MobileInterface.tsx2
52 files changed, 2544 insertions, 500 deletions
diff --git a/src/Utils.ts b/src/Utils.ts
index bfb29fe8d..f2d9e7766 100644
--- a/src/Utils.ts
+++ b/src/Utils.ts
@@ -448,7 +448,7 @@ export function returnEmptyDoclist() { return [] as any[]; }
export let emptyPath = [];
-export function emptyFunction() { }
+export function emptyFunction() { return undefined; }
export function unimplementedFunction() { throw new Error("This function is not implemented, but should be."); }
@@ -658,12 +658,6 @@ export function setupMoveUpEvents(
(target as any)._lastTap = Date.now();
(target as any)._downX = (target as any)._lastX = e.clientX;
(target as any)._downY = (target as any)._lastY = e.clientY;
- if (!(target as any)._doubleTime && noDoubleTapTimeout) {
- (target as any)._doubleTime = setTimeout(() => {
- noDoubleTapTimeout?.();
- (target as any)._doubleTime = undefined;
- }, doubleTapTimeout);
- }
const _moveEvent = (e: PointerEvent): void => {
if (Math.abs(e.clientX - (target as any)._downX) > Utils.DRAG_THRESHOLD || Math.abs(e.clientY - (target as any)._downY) > Utils.DRAG_THRESHOLD) {
@@ -685,6 +679,12 @@ export function setupMoveUpEvents(
const isClick = Math.abs(e.clientX - (target as any)._downX) < 4 && Math.abs(e.clientY - (target as any)._downY) < 4;
upEvent(e, [e.clientX - (target as any)._downX, e.clientY - (target as any)._downY], isClick);
if (isClick) {
+ if (!(target as any)._doubleTime && noDoubleTapTimeout) {
+ (target as any)._doubleTime = setTimeout(() => {
+ noDoubleTapTimeout?.();
+ (target as any)._doubleTime = undefined;
+ }, doubleTapTimeout);
+ }
if ((target as any)._doubleTime && (target as any)._doubleTap) {
clearTimeout((target as any)._doubleTime);
(target as any)._doubleTime = undefined;
diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts
index fc386f81a..5c5818f8f 100644
--- a/src/client/documents/Documents.ts
+++ b/src/client/documents/Documents.ts
@@ -95,6 +95,7 @@ type DROPt = DAInfo | dropActionType;
export class DocumentOptions {
system?: BOOLt = new BoolInfo("is this a system created/owned doc");
_dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else");
+ allowOverlayDrop?: BOOLt = new BoolInfo("can documents be dropped onto this document without using dragging title bar or holding down embed key (ctrl)?");
childDropAction?: DROPt = new DAInfo("what should happen to the source document when it's dropped onto a child of a collection ");
targetDropAction?: DROPt = new DAInfo("what should happen to the source document when ??? ");
color?: string; // foreground color data doc
@@ -293,6 +294,7 @@ export class DocumentOptions {
treeViewExpandedViewLock?: boolean; // whether the expanded view can be changed
treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked
treeViewTruncateTitleWidth?: number;
+ treeViewHasOverlay?: boolean; // whether the treeview has an overlay for freeform annotations
treeViewType?: string; // whether treeview is a Slide, file system, or (default) collection hierarchy
sidebarColor?: string; // background color of text sidebar
sidebarViewType?: string; // collection type of text sidebar
@@ -837,7 +839,7 @@ export namespace Docs {
}
export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
- return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId);
+ return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId);
}
export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) {
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index 435d40d2a..90b43c415 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -339,7 +339,7 @@ export class CurrentUserUtils {
const clickTemplates = CurrentUserUtils.setupClickEditorTemplates(doc);
if (doc.templateDocs === undefined) {
doc.templateDocs = new PrefetchProxy(Docs.Create.TreeDocument([noteTemplates, userTemplateBtns, clickTemplates], {
- title: "template layouts", _xPadding: 0, system: true,
+ title: "template layouts", _xMargin: 0, system: true,
dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name })
}));
}
@@ -421,7 +421,11 @@ export class CurrentUserUtils {
((doc.emptyPane as Doc).proto as Doc)["dragFactory-count"] = 0;
}
if (doc.emptySlide === undefined) {
- const textDoc = Docs.Create.TreeDocument([], { title: "Slide", _viewType: CollectionViewType.Tree, _fontSize: "20px", _autoHeight: true, treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true, backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"]) });
+ const textDoc = Docs.Create.TreeDocument([], {
+ title: "Slide", _viewType: CollectionViewType.Tree, treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true,
+ allowOverlayDrop: true, treeViewType: "outline", _xMargin: 0, _yMargin: 0, _width: 300, _height: 200, _singleLine: true,
+ backgroundColor: "transparent", system: true, cloneFieldFilter: new List<string>(["system"])
+ });
Doc.GetProto(textDoc).title = ComputedField.MakeFunction('self.text?.Text');
FormattedTextBox.SelectOnLoad = textDoc[Id];
doc.emptySlide = textDoc;
@@ -816,7 +820,7 @@ export class CurrentUserUtils {
const newDashboardButton: Doc = Docs.Create.FontIconDocument({ onClick: newDashboard, _forceActive: true, toolTip: "Create new dashboard", _stayInCollection: true, _hideContextMenu: true, title: "new dashboard", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true });
doc.myDashboards = new PrefetchProxy(Docs.Create.TreeDocument([], {
title: "My Dashboards", _showTitle: "title", _height: 400, childHideLinkButton: true, freezeChildren: "remove|add",
- treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+ treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newDashboardButton,
_lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", treeViewType: "fileSystem", isFolder: true, system: true,
explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files."
@@ -845,7 +849,7 @@ export class CurrentUserUtils {
const newTrailButton: Doc = Docs.Create.FontIconDocument({ onClick: newTrail, _forceActive: true, toolTip: "Create new trail", _stayInCollection: true, _hideContextMenu: true, title: "New trail", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "New trail", icon: "plus", system: true });
doc.myTrails = new PrefetchProxy(Docs.Create.TreeDocument([], {
title: "My Trails", _showTitle: "title", _height: 100,
- treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+ treeViewHideTitle: true, _fitWidth: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
treeViewTruncateTitleWidth: 150, ignoreClick: true, buttonMenu: true, buttonMenuDoc: newTrailButton,
_lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true,
explainer: "All of the trails that you have created will appear here."
@@ -870,7 +874,7 @@ export class CurrentUserUtils {
});
doc.myFilesystem = new PrefetchProxy(Docs.Create.TreeDocument([doc.myFileOrphans as Doc], {
title: "My Documents", _showTitle: "title", buttonMenu: true, buttonMenuDoc: newFolderButton, _height: 100,
- treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+ treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
treeViewTruncateTitleWidth: 150, ignoreClick: true,
isFolder: true, treeViewType: "fileSystem", childHideLinkButton: true,
_lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "proto", system: true,
@@ -889,7 +893,7 @@ export class CurrentUserUtils {
const clearDocsButton: Doc = Docs.Create.FontIconDocument({ onClick: clearAll, _forceActive: true, toolTip: "Empty recently closed", _stayInCollection: true, _hideContextMenu: true, title: "Empty", btnType: ButtonType.ClickButton, _width: 30, _height: 30, buttonText: "Empty", icon: "trash", system: true });
doc.myRecentlyClosedDocs = new PrefetchProxy(Docs.Create.TreeDocument([], {
title: "My Recently Closed", _showTitle: "title", buttonMenu: true, buttonMenuDoc: clearDocsButton, childHideLinkButton: true,
- treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "alias",
+ treeViewHideTitle: true, _gridGap: 5, _forceActive: true, childDropAction: "alias",
treeViewTruncateTitleWidth: 150, ignoreClick: true,
_lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true,
explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list."
@@ -907,7 +911,7 @@ export class CurrentUserUtils {
if (doc.currentFilter === undefined) {
doc.currentFilter = Docs.Create.FilterDocument({
title: "Unnamed Filter", _height: 150,
- treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, childDropAction: "none",
+ treeViewHideTitle: true, _xPadding: 5, _yPadding: 5, _gridGap: 5, _forceActive: true, childDropAction: "none",
treeViewTruncateTitleWidth: 150, ignoreClick: true,
_lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true, _autoHeight: true, _fitWidth: true
});
@@ -923,7 +927,7 @@ export class CurrentUserUtils {
doc.treeViewOpen = true;
doc.treeViewExpandedView = "fields";
doc.myUserDoc = new PrefetchProxy(Docs.Create.TreeDocument([doc], {
- treeViewHideTitle: true, _xMargin: 5, _yMargin: 5, _gridGap: 5, _forceActive: true, title: "My UserDoc", _showTitle: "title",
+ treeViewHideTitle: true, _gridGap: 5, _forceActive: true, title: "My UserDoc", _showTitle: "title",
treeViewTruncateTitleWidth: 150, ignoreClick: true,
_lockedPosition: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", system: true
})) as any as Doc;
@@ -1006,6 +1010,7 @@ export class CurrentUserUtils {
static inkTools(doc: Doc) {
const tools: Button[] = [
{ title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen", click: 'setActiveInkTool("pen")', checkResult: 'setActiveInkTool("pen" , true)' },
+ { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", click: 'setActiveInkTool("eraser")', checkResult: 'setActiveInkTool("eraser" , true)' },
// { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", click: 'setActiveInkTool("highlighter")', checkResult: 'setActiveInkTool("highlighter", true)' },
{ title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle", click: 'setActiveInkTool("circle")', checkResult: 'setActiveInkTool("circle" , true)' },
// { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square", click: 'setActiveInkTool("square")', checkResult: 'setActiveInkTool("square" , true)' },
diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts
index f5704d2bf..ae3fa3170 100644
--- a/src/client/util/DragManager.ts
+++ b/src/client/util/DragManager.ts
@@ -335,8 +335,10 @@ export namespace DragManager {
}
export let docsBeingDragged: Doc[] = [];
export let CanEmbed = false;
+ export let DocDragData: DocumentDragData | undefined;
export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) {
if (dragData.dropAction === "none") return;
+ DocDragData = dragData instanceof DocumentDragData ? dragData : undefined;
const batch = UndoManager.StartBatch("dragging");
eles = eles.filter(e => e);
CanEmbed = dragData.canEmbed || false;
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx
index a32a8eecc..61872417b 100644
--- a/src/client/util/InteractionUtils.tsx
+++ b/src/client/util/InteractionUtils.tsx
@@ -91,7 +91,8 @@ export namespace InteractionUtils {
export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number,
color: string, width: number, strokeWidth: number, lineJoin: string, lineCap: string, bezier: string, fill: string, arrowStart: string, arrowEnd: string,
- dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean) {
+ markerScale: number, dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean,
+ downHdlr?: ((e: React.PointerEvent) => void)) {
const pts = shape ? makePolygon(shape, points) : points;
if (isNaN(scalex)) scalex = 1;
@@ -107,19 +108,24 @@ export namespace InteractionUtils {
const Tag = (bezier ? "path" : "polyline") as keyof JSX.IntrinsicElements;
const makerStrokeWidth = strokeWidth / 2;
- return (<svg fill={color}> {/* setting the svg fill sets the arrowStart fill */}
+ const arrowWidthFactor = 3 * (markerScale ? markerScale : 0.5);// used to be 1.5
+ const arrowLengthFactor = 5 * (markerScale ? markerScale : 0.5);
+ const arrowNotchFactor = 2 * (markerScale ? markerScale : 0.5);
+ return (<svg fill={color} onPointerDown={downHdlr}> {/* setting the svg fill sets the arrowStart fill */}
{nodefs ? (null) : <defs>
{arrowStart !== "dot" && arrowEnd !== "dot" ? (null) :
<marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible">
<circle r={strokeWidth * 1.5} fill="context-stroke" />
</marker>}
{arrowStart !== "arrow" ? (null) :
- <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * 1.5} refY={0} markerWidth="10" markerHeight="7">
- <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3} points={`${3 * makerStrokeWidth} ${-makerStrokeWidth * 1.5}, ${makerStrokeWidth * 2} 0, ${3 * makerStrokeWidth} ${makerStrokeWidth * 1.5}, 0 0`} />
+ <marker id={`arrowStart${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} refY={0} markerWidth="10" markerHeight="7">
+ <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3}
+ points={`${arrowLengthFactor * makerStrokeWidth} ${-makerStrokeWidth * arrowWidthFactor}, ${makerStrokeWidth * (arrowLengthFactor - arrowNotchFactor)} 0, ${arrowLengthFactor * makerStrokeWidth} ${makerStrokeWidth * arrowWidthFactor}, 0 0`} />
</marker>}
{arrowEnd !== "arrow" ? (null) :
- <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * 1.5} refY={0} markerWidth="10" markerHeight="7">
- <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3} points={`0 ${-makerStrokeWidth * 1.5}, ${makerStrokeWidth} 0, 0 ${makerStrokeWidth * 1.5}, ${3 * makerStrokeWidth} 0`} />
+ <marker id={`arrowEnd${defGuid}`} markerUnits="userSpaceOnUse" orient="auto" overflow="visible" refX={makerStrokeWidth * arrowNotchFactor} refY={0} markerWidth="10" markerHeight="7">
+ <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} strokeWidth={makerStrokeWidth * 2 / 3}
+ points={`0 ${-makerStrokeWidth * arrowWidthFactor}, ${makerStrokeWidth * arrowNotchFactor} 0, 0 ${makerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * makerStrokeWidth} 0`} />
</marker>}
</defs>}
diff --git a/src/client/util/bezierFit.ts b/src/client/util/bezierFit.ts
new file mode 100644
index 000000000..8fc6de6f9
--- /dev/null
+++ b/src/client/util/bezierFit.ts
@@ -0,0 +1,1431 @@
+import { Point } from "../../pen-gestures/ndollar";
+
+class SmartRect {
+ minx: number = 0;
+ miny: number = 0;
+ maxx: number = 0;
+ maxy: number = 0;
+
+ constructor(mix: number = 0, miy: number = 0, max: number = 0, may: number = 0) { this.minx = mix; this.miny = miy; this.maxx = max; this.maxy = may; }
+
+ public get Center() { return new Point((this.maxx + this.minx) / 2.0, (this.maxy + this.miny) / 2.0); }
+ public get TopLeft() { return new Point(this.minx, this.miny); }
+ public get TopRight() { return new Point(this.maxx, this.miny); }
+ public get BotLeft() { return new Point(this.minx, this.maxy); }
+ public get BotRight() { return new Point(this.maxx, this.maxy); }
+ public get Width() { return this.maxx - this.minx; }
+ public get Height() { return this.maxy - this.miny; }
+ public static Intersect(a: SmartRect, b: SmartRect) { return a.Intersect(b); }
+ public Intersect(b: SmartRect) { return !((this.minx > b.maxx) || (this.miny > b.maxy) || (b.minx > this.maxx) || (b.miny > this.maxy)); }
+
+ public ContainsPercentage(other: SmartRect, axis: Point) {
+ var ret = 0;
+ const minx = Math.max(other.TopLeft.X * axis.X + other.TopLeft.Y * axis.Y, this.TopLeft.X * axis.X + this.TopLeft.Y * axis.Y);
+ const maxx = Math.max(other.BotRight.X * axis.X + other.BotRight.Y * axis.Y, this.BotRight.X * axis.X + this.BotRight.Y * axis.Y);
+ ret = maxx > minx ? (maxx - minx) / (axis === new Point(1, 0) ? other.Width : other.Height) : 0;
+ return ret;
+ }
+ public static Bounds(p: Point[]) {
+ const r = new SmartRect();
+ if (p.length > 0) {
+ r.minx = p[0].X; // These are the most likely to be extremal
+ r.maxx = p.lastElement().X;
+ r.miny = p[0].Y;
+ r.maxy = p.lastElement().Y;
+
+ if (r.minx > r.maxx) [r.minx, r.maxx] = [r.maxx, r.minx];
+ if (r.miny > r.maxy) [r.miny, r.maxy] = [r.maxy, r.miny];
+
+ for (const pt of p) {
+ if (pt.X < r.minx) {
+ r.minx = pt.X;
+ } else if (pt.X > r.maxx) {
+ r.maxx = pt.X;
+ }
+ if (pt.Y < r.miny) {
+ r.miny = pt.Y;
+ } else if (pt.Y > r.maxy) {
+ r.maxy = pt.Y;
+ }
+ }
+ }
+ return r;
+ }
+}
+
+export function Distance(p: Point) {
+ return Math.sqrt(p.X * p.X + p.Y * p.Y);
+}
+export function Normalize(p: Point) {
+ const len = Distance(p);
+ return new Point(p.X / len, p.Y / len);
+}
+
+function ReparameterizeBezier(d: Point[], first: number, last: number, u: number[], bezCurve: Point[]) {
+ const uPrime = new Array<number>(last - first + 1); // New parameter values
+
+ for (var i = first; i <= last; i++) {
+ uPrime[i - first] = NewtonRaphsonRootFind(bezCurve, d[i], u[i - first]);
+ }
+ return uPrime;
+}
+function ComputeMaxError(d: Point[], first: number, last: number, bezCurve: Point[], u: number[]) {
+ var maxError = 0; // Maximum error
+ var splitPoint2D = (last - first + 1) / 2;
+ for (var i = first + 1; i < last; i++) {
+ const P = [0, 0]; // point on curve
+ EvalBezierFast(bezCurve, u[i - first], P);
+ const dx = P[0] - d[i].X;// offset from point to curve
+ const dy = P[1] - d[i].Y;
+ const dist = Math.sqrt(dx * dx + dy * dy); // Current error
+ if (dist >= maxError) {
+ maxError = dist;
+ if (splitPoint2D) {
+ splitPoint2D = i;
+ }
+ }
+ }
+ return { maxError, splitPoint2D };
+}
+function ChordLengthParameterize(d: Point[], first: number, last: number) {
+ const u = new Array<number>(last - first + 1);// Parameterization
+
+ var prev = 0.0;
+ u[0] = prev;
+ for (var i = first + 1; i <= last; i++) {
+ const lastd = d[i - 1];
+ const curd = d[i];
+ const dx = lastd.X - curd.X;
+ const dy = lastd.Y - curd.Y;
+ prev = u[i - first] = prev + Math.sqrt(dx * dx + dy * dy);
+ }
+
+ const ulastfirst = u[last - first];
+ for (var i = first + 1; i <= last; i++) {
+ u[i - first] /= ulastfirst;
+ }
+
+ return u;
+}
+/*
+* B0, B1, B2, B3 :
+* Bezier multipliers
+*/
+function B0(u: number) { const tmp = 1.0 - u; return tmp * tmp * tmp; }
+function B1(u: number) { const tmp = 1.0 - u; return 3 * u * tmp * tmp; }
+function B2(u: number) { const tmp = 1.0 - u; return 3 * u * u * tmp; }
+function B3(u: number) { return u * u * u; }
+function bounds(p: Point[]) {
+ const r = new SmartRect(p[0].X, p[0].Y, p[3].X, p[3].Y); // These are the most likely to be extremal
+
+ if (r.minx > r.maxx) (r.minx, r.maxx);
+ if (r.miny > r.maxy) [r.miny, r.maxy] = [r.maxy, r.miny]; // swap min & max
+
+ for (var i = 1; i < 3; i++) {
+ if (p[i].X < r.minx) r.minx = p[i].X;
+ else if (p[i].X > r.maxx) r.maxx = p[i].X;
+
+ if (p[i].Y < r.miny) r.miny = p[i].Y;
+ else if (p[i].Y > r.maxy) r.maxy = p[i].Y;
+ }
+ return r;
+}
+
+
+function splitCubic(p: Point[], t: number, left: Point[], right: Point[]) {
+ const sz = 4;
+ const Vtemp = new Array<Array<Point>>(4);
+ for (var i = 0; i < 4; i++) Vtemp[i] = new Array<Point>(4);
+
+ /* Copy control points */
+ // std::copy(p.begin(), p.end(), Vtemp[0]);
+ for (var i = 0; i < sz; i++) {
+ Vtemp[0][i].X = p[i].X;
+ Vtemp[0][i].Y = p[i].Y;
+ }
+
+ /* Triangle computation */
+ for (var i = 1; i < sz; i++) {
+ for (var j = 0; j < sz - i; j++) {
+ const a = Vtemp[i - 1][j];
+ const b = Vtemp[i - 1][j + 1];
+ Vtemp[i][j].X = b.X * t + a.X * (1 - t);
+ Vtemp[i][j].Y = b.Y * t + a.Y * (1 - t); // Vtemp[i][j] = Point2D::Lerp(Vtemp[i - 1][j], Vtemp[i - 1][j + 1], t);
+ }
+ }
+
+ if (left) {
+ for (var j = 0; j < sz; j++) {
+ left[j].X = Vtemp[j][0].X;
+ left[j].Y = Vtemp[j][0].Y;
+ }
+ }
+ if (right) {
+ for (var j = 0; j < sz; j++) {
+ right[j].X = Vtemp[sz - 1 - j][j].X;
+ right[j].Y = Vtemp[sz - 1 - j][j].Y;
+ }
+ }
+}
+
+/*
+* Recursively intersect two curves keeping track of their real parameters
+* and depths of intersection.
+* The results are returned in a 2-D array of doubles indicating the parameters
+* for which intersections are found. The parameters are in the order the
+* intersections were found, which is probably not in sorted order.
+* When an intersection is found, the parameter value for each of the two
+* is stored in the index elements array, and the index is incremented.
+*
+* If either of the curves has subdivisions left before it is straight
+* (depth > 0)
+* that curve (possibly both) is (are) subdivided at its (their) midpoint(s).
+* the depth(s) is (are) decremented, and the parameter value(s) corresponding
+* to the midpoints(s) is (are) computed.
+* Then each of the subcurves of one curve is intersected with each of the
+* subcurves of the other curve, first by testing the bounding boxes for
+* interference. If there is any bounding box interference, the corresponding
+* subcurves are recursively intersected.
+*
+* If neither curve has subdivisions left, the line segments from the first
+* to last control point of each segment are intersected. (Actually the
+* only the parameter value corresponding to the intersection point is found).
+*
+* The apriori flatness test is probably more efficient than testing at each
+* level of recursion, although a test after three or four levels would
+* probably be worthwhile, since many curves become flat faster than their
+* asymptotic rate for the first few levels of recursion.
+*
+* The bounding box test fails much more frequently than it succeeds, providing
+* substantial pruning of the search space.
+*
+* Each (sub)curve is subdivided only once, hence it is not possible that for
+* one final line intersection test the subdivision was at one level, while
+* for another final line intersection test the subdivision (of the same curve)
+* was at another. Since the line segments share endpoints, the intersection
+* is robust: a near-tangential intersection will yield zero or two
+* intersections.
+*/
+function recursively_intersect(a: Point[], t0: number, t1: number, deptha: number, b: Point[], u0: number, u1: number, depthb: number, parameters: number[][]) {
+ if (deptha > 0) {
+ const a1 = new Array<Point>(4), a2 = new Array<Point>(4);
+ splitCubic(a, 0.5, a1, a2);
+ const tmid = (t0 + t1) * 0.5;
+ deptha--;
+ if (depthb > 0) {
+ const b1 = new Array<Point>(4), b2 = new Array<Point>(4);
+ splitCubic(b, 0.5, b1, b2);
+ const umid = (u0 + u1) * 0.5;
+ depthb--;
+ if (SmartRect.Intersect(bounds(a1), bounds(b1))) {
+ recursively_intersect(a1, t0, tmid, deptha, b1, u0, umid, depthb, parameters);
+ }
+ if (SmartRect.Intersect(bounds(a2), bounds(b1))) {
+ recursively_intersect(a2, tmid, t1, deptha, b1, u0, umid, depthb, parameters);
+ }
+ if (SmartRect.Intersect(bounds(a1), bounds(b2))) {
+ recursively_intersect(a1, t0, tmid, deptha, b2, umid, u1, depthb, parameters);
+ }
+ if (SmartRect.Intersect(bounds(a2), bounds(b2))) {
+ recursively_intersect(a2, tmid, t1, deptha, b2, umid, u1, depthb, parameters);
+ }
+ }
+ else {
+ if (SmartRect.Intersect(bounds(a1), bounds(b))) {
+ recursively_intersect(a1, t0, tmid, deptha, b, u0, u1, depthb, parameters);
+ }
+ if (SmartRect.Intersect(bounds(a2), bounds(b))) {
+ recursively_intersect(a2, tmid, t1, deptha, b, u0, u1, depthb, parameters);
+ }
+ }
+ }
+ else {
+ if (depthb > 0) {
+ const b1 = new Array<Point>(4), b2 = new Array<Point>(4);
+ splitCubic(b, 0.5, b1, b2);
+ const umid = (u0 + u1) * 0.5;
+ depthb--;
+ if (SmartRect.Intersect(bounds(a), bounds(b1))) {
+ recursively_intersect(a, t0, t1, deptha, b1, u0, umid, depthb, parameters);
+ }
+ if (SmartRect.Intersect(bounds(a), bounds(b2))) {
+ recursively_intersect(a, t0, t1, deptha, b2, umid, u1, depthb, parameters);
+ }
+ }
+ else // Both segments are fully subdivided; now do line segments
+ {
+ const xlk = a[3].X - a[0].X;
+ const ylk = a[3].Y - a[0].Y;
+ const xnm = b[3].X - b[0].X;
+ const ynm = b[3].Y - b[0].Y;
+ const xmk = b[0].X - a[0].X;
+ const ymk = b[0].Y - a[0].Y;
+ const det = xnm * ylk - ynm * xlk;
+ if (1.0 + det === 1.0) {
+ return;
+ }
+ else {
+ const detinv = 1.0 / det;
+ const s = (xnm * ymk - ynm * xmk) * detinv;
+ const t = (xlk * ymk - ylk * xmk) * detinv;
+ if ((s < 0.0) || (s > 1.0) || (t < 0.0) || (t > 1.0) || Number.isNaN(s) || Number.isNaN(t)) {
+ return;
+ }
+ parameters.push([t0 + s * (t1 - t0), u0 + t * (u1 - u0)]);
+ }
+ }
+ }
+}
+
+/*
+* EvalBezier :
+* Evaluate a Bezier curve at a particular parameter value
+*
+*/
+const MAX_DEGREE = 5;
+function EvalBezier(V: Point[], degree: number, t: number, result: number[]) {
+ if (degree + 1 > MAX_DEGREE) {
+ result[0] = V[0].X;
+ result[1] = V[0].Y;
+ return;
+ }
+
+ const Vtemp = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)]; // Local copy of control points
+
+ /* Copy array */
+ for (var i = 0; i <= degree; i++) {
+ Vtemp[i].X = V[i].X;
+ Vtemp[i].Y = V[i].Y;
+ }
+
+ /* Triangle computation */
+ for (var i = 1; i <= degree; i++) {
+ for (var j = 0; j <= degree - i; j++) {
+ Vtemp[j].X = (1.0 - t) * Vtemp[j].X + t * Vtemp[j + 1].X;
+ Vtemp[j].Y = (1.0 - t) * Vtemp[j].Y + t * Vtemp[j + 1].Y;
+ }
+ }
+
+ result[0] = Vtemp[0].X;
+ result[1] = Vtemp[0].Y;// Point on curve at parameter t
+}
+
+function EvalBezierFast(p: Point[], t: number, result: number[]) {
+ const n = 3;
+ const u = 1.0 - t;
+ var bc = 1, tn = 1;
+ var tmpX = p[0].X * u;
+ var tmpY = p[0].Y * u;
+ tn = tn * t;
+ bc = bc * (n - 1 + 1) / 1;
+ tmpX = (tmpX + tn * bc * p[1].X) * u;
+ tmpY = (tmpY + tn * bc * p[1].Y) * u;
+ tn = tn * t;
+ bc = bc * (n - 2 + 1) / 2;
+ tmpX = (tmpX + tn * bc * p[2].X) * u;
+ tmpY = (tmpY + tn * bc * p[2].Y) * u;
+
+ result[0] = tmpX + tn * t * p[3].X;
+ result[1] = tmpY + tn * t * p[3].Y;
+}
+/*
+* ComputeLeftTangent, ComputeRightTangent, ComputeCenterTangent :
+*Approximate unit tangents at endpoints and "center" of digitized curve
+*/
+function ComputeLeftTangent(d: Point[], end: number) {
+ const use = 1;
+ const tHat1 = new Point(d[end + use].X - d[end].X, d[end + use].Y - d[end].Y);
+ return Normalize(tHat1);
+}
+function ComputeRightTangent(d: Point[], end: number) {
+ const use = 1;
+ const tHat2 = new Point(d[end - use].X - d[end].X, d[end - use].Y - d[end].Y);
+ return Normalize(tHat2);
+}
+function ComputeCenterTangent(d: Point[], center: number) {
+ if (center === 0) {
+ return ComputeLeftTangent(d, center);
+ }
+ const V1 = ComputeLeftTangent(d, center); // d[center] - d[center-1];
+ const V2 = ComputeRightTangent(d, center); // d[center] - d[center + 1];
+ var tHatCenter = new Point((-V1.X + V2.X) / 2.0, (-V1.Y + V2.Y) / 2.0);
+ if (tHatCenter === new Point(0, 0)) {
+ tHatCenter = new Point(-V1.Y, -V1.X);// V1.Perp();
+ }
+ return Normalize(tHatCenter);
+}
+function GenerateBezier(d: Point[], first: number, last: number, uPrime: number[], tHat1: Point, tHat2: Point, result: Point[] /* must be prealloacted to size 4 */) {
+ const nPts = last - first + 1; // Number of pts in sub-curve
+ const Ax = new Array<number>(nPts * 2);// Precomputed rhs for eqn //std::vector<Vector2D> A(nPts * 2);
+ const Ay = new Array<number>(nPts * 2);// Precomputed rhs for eqn //std::vector<Vector2D> A(nPts * 2);
+
+ /* Compute the A's */
+ for (var i = 0; i < nPts; i++) {
+ const uprime = uPrime[i];
+ const b1 = B1(uprime);
+ const b2 = B2(uprime);
+ Ax[i] = tHat1.X * b1;
+ Ay[i] = tHat1.Y * b1;
+ Ax[i + 1 * nPts] = tHat2.X * b2;
+ Ay[i + 1 * nPts] = tHat2.Y * b2;
+ }
+
+ /* Create the C and X matrices */
+ const C = [[0, 0], [0, 0]];
+ const df = d[first];
+ const dl = d[last];
+
+ const X = [0, 0]; // Matrix X
+ for (var i = 0; i < nPts; i++) {
+ C[0][0] += Ax[i] * Ax[i] + Ay[i] * Ay[i]; //A[i+0*nPts].Dot(A[i+0*nPts]);
+ C[0][1] += Ax[i] * Ax[i + nPts] + Ay[i] * Ay[i + nPts];//A[i+0*nPts].Dot(A[i+1*nPts]);
+ C[1][0] = C[0][1];
+ C[1][1] += Ax[i + nPts] * Ax[i + nPts] + Ay[i + nPts] * Ay[i + nPts];// A[i+1*nPts].Dot(A[i+1*nPts]);
+ const uprime = uPrime[i];
+ const b0plb1 = B0(uprime) + B1(uprime);
+ const b2plb3 = B2(uprime) + B3(uprime);
+ const df1 = d[first + i];
+ const tmpX = df1.X - (df.X * b0plb1 + (dl.X * b2plb3));
+ const tmpY = df1.Y - (df.Y * b0plb1 + (dl.Y * b2plb3));
+
+ X[0] += Ax[i] * tmpX + Ay[i] * tmpY; // A[i+0*nPts].Dot(tmp)
+ X[1] += Ax[i + nPts] * tmpX + Ay[i + nPts] * tmpY; //A[i+1*nPts].Dot(tmp)
+ }
+
+ /* Compute the determinants of C and X */
+ const det_C0_C1 = (C[0][0] * C[1][1] - C[1][0] * C[0][1]) || (C[0][0] * C[1][1]) * 10e-12;
+ const det_C0_X = C[0][0] * X[1] - C[0][1] * X[0];
+ const det_X_C1 = X[0] * C[1][1] - X[1] * C[0][1];
+
+ /* Finally, derive alpha values */
+ var alpha_l = (det_C0_C1 === 0) ? 0.0 : det_X_C1 / det_C0_C1;
+ var alpha_r = (det_C0_C1 === 0) ? 0.0 : det_C0_X / det_C0_C1;
+
+ /* If alpha negative, use the Wu/Barsky heuristic (see text) */
+ /* (if alpha is 0, you get coincident control points that lead to
+ * divide by zero in any subsequent NewtonRaphsonRootFind() call. */
+ const segLength = Math.sqrt((df.X - dl.X) * (df.X - dl.X) + (df.Y - dl.Y) * (df.Y - dl.Y));
+ const epsilon = 1.0e-6 * segLength;
+ if (alpha_l < epsilon || alpha_r < epsilon) {
+ /* fall back on standard (probably inaccurate) formula, and subdivide further if needed. */
+ alpha_l = alpha_r = segLength / 3.0;
+ }
+
+ /* First and last control points of the Bezier curve are */
+ /* positioned exactly at the first and last data points */
+ /* Control points 1 and 2 are positioned an alpha distance out */
+ /* on the tangent vectors, left and right, respectively */
+ result[0] = df;// RETURN bezier curve ctl pts
+ result[3] = dl;
+ result[1] = new Point(df.X + (tHat1.X * alpha_l), df.Y + (tHat1.Y * alpha_l));
+ result[2] = new Point(dl.X + (tHat2.X * alpha_r), dl.Y + (tHat2.Y * alpha_r));
+}
+
+/*
+ * NewtonRaphsonRootFind :
+ * Use Newton-Raphson iteration to find better root.
+ */
+function NewtonRaphsonRootFind(Q: Point[], P: Point, u: number) {
+ const Q1 = [new Point(0, 0), new Point(0, 0), new Point(0, 0)], Q2 = [new Point(0, 0), new Point(0, 0)]; // Q' and Q''
+ const Q_u = [0, 0], Q1_u = [0, 0], Q2_u = [0, 0]; //u evaluated at Q, Q', & Q''
+
+ /* Compute Q(u) */
+ var uPrime: number; // Improved u
+ EvalBezierFast(Q, u, Q_u);
+
+ /* Generate control vertices for Q' */
+ for (var i = 0; i <= 2; i++) {
+ Q1[i].X = (Q[i + 1].X - Q[i].X) * 3.0;
+ Q1[i].Y = (Q[i + 1].Y - Q[i].Y) * 3.0;
+ }
+
+ /* Generate control vertices for Q'' */
+ for (var i = 0; i <= 1; i++) {
+ Q2[i].X = (Q1[i + 1].X - Q1[i].X) * 2.0;
+ Q2[i].Y = (Q1[i + 1].Y - Q1[i].Y) * 2.0;
+ }
+
+ /* Compute Q'(u) and Q''(u) */
+ EvalBezier(Q1, 2, u, Q1_u);
+ EvalBezier(Q2, 1, u, Q2_u);
+
+ /* Compute f(u)/f'(u) */
+ const numerator = (Q_u[0] - P.X) * (Q1_u[0]) + (Q_u[1] - P.Y) * (Q1_u[1]);
+ const denominator = (Q1_u[0]) * (Q1_u[0]) + (Q1_u[1]) * (Q1_u[1]) + (Q_u[0] - P.X) * (Q2_u[0]) + (Q_u[1] - P.Y) * (Q2_u[1]);
+ if (denominator === 0.0) {
+ uPrime = u;
+ } else uPrime = u - (numerator / denominator);/* u = u - f(u)/f'(u) */
+
+ return uPrime;
+}
+function FitCubic(d: Point[], first: number, last: number, tHat1: Point, tHat2: Point, error: number, result: Point[]) {
+ const bezCurve = new Array<Point>(4); // Control points of fitted Bezier curve
+ const maxIterations = 4; // Max times to try iterating
+
+ const iterationError = error * error; // Error below which you try iterating
+ const nPts = last - first + 1; // Number of points in subset
+
+ /* Use heuristic if region only has two points in it */
+ if (nPts === 2) {
+ const dist = Math.sqrt((d[first].X - d[last].X) * (d[first].X - d[last].X) + (d[first].Y - d[last].Y) * (d[first].Y - d[last].Y)) / 3;
+
+ bezCurve[0] = d[first];
+ bezCurve[3] = d[last];
+ bezCurve[1] = new Point(bezCurve[0].X + (tHat1.X * dist), bezCurve[0].Y + (tHat1.Y * dist));
+ bezCurve[2] = new Point(bezCurve[3].X + (tHat2.X * dist), bezCurve[3].Y + (tHat2.Y * dist));
+
+ result.push(bezCurve[1]);
+ result.push(bezCurve[2]);
+ result.push(bezCurve[3]);
+ return;
+ }
+
+ /* Parameterize points, and attempt to fit curve */
+ var u = ChordLengthParameterize(d, first, last);
+ GenerateBezier(d, first, last, u, tHat1, tHat2, bezCurve);
+
+ /* Find max deviation of points to fitted curve */
+ const { maxError, splitPoint2D } = ComputeMaxError(d, first, last, bezCurve, u); // Maximum fitting error
+ if (maxError < Math.abs(error)) {
+ result.push(bezCurve[1]);
+ result.push(bezCurve[2]);
+ result.push(bezCurve[3]);
+ return;
+ }
+
+ /* If error not too large, try some reparameterization */
+ /* and iteration */
+ if (maxError < iterationError) {
+ for (var i = 0; i < maxIterations; i++) {
+ const uPrime = ReparameterizeBezier(d, first, last, u, bezCurve); // Improved parameter values
+ GenerateBezier(d, first, last, uPrime, tHat1, tHat2, bezCurve);
+ const { maxError } = ComputeMaxError(d, first, last, bezCurve, uPrime);
+ if (maxError < error) {
+ result.push(bezCurve[1]);
+ result.push(bezCurve[2]);
+ result.push(bezCurve[3]);
+ return;
+ }
+ u = uPrime;
+ }
+ }
+
+ /* Fitting failed -- split at max error point and fit recursively */
+ const tHatCenter = splitPoint2D >= last - 1 ? ComputeRightTangent(d, splitPoint2D) : ComputeCenterTangent(d, splitPoint2D);
+ FitCubic(d, first, splitPoint2D, tHat1, tHatCenter, error, result);
+ const negThatCenter = new Point(-tHatCenter.X, -tHatCenter.Y);
+ FitCubic(d, splitPoint2D, last, negThatCenter, tHat2, error, result);
+}
+export function FitCurve(d: Point[], error: number) {
+ const tHat1 = ComputeLeftTangent(d, 0); // Unit tangent vectors at endpoints
+ const tHat2 = ComputeRightTangent(d, d.length - 1);
+ const result = [d[0]];
+ FitCubic(d, 0, d.length - 1, tHat1, tHat2, error, result);
+ return result;
+}
+export function FitOneCurve(d: Point[], tHat1?: Point, tHat2?: Point) {
+ tHat1 = tHat1 ?? Normalize(ComputeLeftTangent(d, 0));
+ tHat2 = tHat2 ?? Normalize(ComputeRightTangent(d, d.length - 1));
+ tHat2 = new Point(-tHat2.X, -tHat2.Y);
+ var u = ChordLengthParameterize(d, 0, d.length - 1);
+ const bezCurveCtrls = [new Point(0, 0), new Point(0, 0), new Point(0, 0), new Point(0, 0)];
+ GenerateBezier(d, 0, d.length - 1, u, tHat1, tHat2, bezCurveCtrls); /* Find max deviation of points to fitted curve */
+ var finalCtrls = bezCurveCtrls.slice();
+ var { maxError: error } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, u);
+ for (var i = 0; i < 10; i++) {
+ const uPrime = ReparameterizeBezier(d, 0, d.length - 1, u, bezCurveCtrls); // Improved parameter values
+ GenerateBezier(d, 0, d.length - 1, uPrime, tHat1, tHat2, bezCurveCtrls);
+ const { maxError } = ComputeMaxError(d, 0, d.length - 1, bezCurveCtrls, uPrime);
+ if (maxError < error) {
+ error = maxError;
+ finalCtrls = bezCurveCtrls.slice();
+ }
+ u = uPrime;
+ }
+ return { finalCtrls, error };
+}
+
+/*
+static double GetTValueFromSValue (const BezierRep &parent, double t, double endT, bool left, double influenceDistance, double &excess) {
+double dist = 0;
+double step = 0.01;
+double spliceT = t;
+for (spliceT = t+(left?-1:1)*step; dist < influenceDistance && (left ? (spliceT > endT) : (spliceT < endT)); spliceT += step * (left ? -1 : 1)) {
+dist += (parent[spliceT]-parent[spliceT-step*(left ? -1:1)]).Length();
+}
+if ((left && spliceT < endT) || (!left && spliceT > endT))
+spliceT = endT;
+excess = influenceDistance - dist;
+return spliceT;
+}
+static BezierRep::BezierLock FindSplitIndex (const BezierRep &parent, double t, bool left, std::vector<BezierRep::BezierLock> &locked)
+{
+BezierRep::BezierLock cuspIndex = { left ? 0.0 : 1.0*parent.MaxIndex(), true};
+double tprev = t;
+for (int tstep = (left ? std::floor(t) : std::ceil(t)) * 3; left ? (tstep >= 0) : (tstep < parent.p.size()); tstep += (left ? -1 : 1))
+{
+double near = HUGE_VAL;
+for (auto &l : locked) {
+if ((( left && tprev > l.T && tstep <= l.T) ||
+(!left && tprev < l.T && tstep >= l.T)) && std::abs(tprev-l.T) < near) {
+near = std::abs(tprev-l.T);
+cuspIndex = l;
+}
+}
+if (near != HUGE_VAL)
+break;
+}
+return cuspIndex;
+}
+size_t SampleBezier (const BezierRep &bez, Point2D *&multiSegmentSamplePts, size_t numMultiSegmentSamples, size_t samplesPerSegment)
+{
+auto numSamples = bez.MaxIndex() * samplesPerSegment + 1;
+if (numSamples > numMultiSegmentSamples) {
+if (numMultiSegmentSamples)
+delete [] multiSegmentSamplePts;
+multiSegmentSamplePts = new Point2D[numSamples];
+}
+for (auto seg = 0; seg < bez.MaxIndex(); seg++)
+{
+ double result[2];
+Point2D tmp[4] = { bez.p[seg * 3], bez.p[seg * 3 +1], bez.p[seg * 3 +2], bez.p[seg * 3 +3] };
+for (auto index = 0; index < samplesPerSegment; index++) {
+EvalBezierFast(tmp, 1.0 * index / samplesPerSegment, result);
+multiSegmentSamplePts[seg * samplesPerSegment + index].X = result[0];
+multiSegmentSamplePts[seg * samplesPerSegment + index].Y = result[1];
+}
+}
+multiSegmentSamplePts[numSamples-1] = bez.p.back();
+return numSamples;
+}
+static double GetSpliceCurve (const BezierRep &parent, double t, BezierRep::BezierLock * isCusp, BezierRep::BezierLock tEnd, std::vector<BezierRep::BezierLock> &locked, const Vector2D &v, Point2D singleSegmentSpliceCurve[4], double errorTolerance, double influenceDistance, double &excess)
+{
+Point2D *multiSegmentSamplePts = NULL;
+size_t numMultiSegmentSamples = 0;
+double spliceT = tEnd.T;
+bool left = tEnd.T < t;
+auto parTangent = parent.Tangent(t + (left ? -1e-7:1e-7));
+if (_isnan(parTangent.X))
+parTangent = Vector2D();
+for (auto &l : locked) {
+if (l.T == t && l.Cusp) {
+parTangent = Vector2D();
+if (left && (l.Side == 2) && t<= parent.MaxIndex())
+parTangent = (parent[t+1]-(parent[t]+v)).Normal();
+else if (!left && (l.Side == 1) && t >= 1)
+parTangent = (parent[t]+v - parent[t-1]).Normal();
+}
+}
+
+if (_isnan(influenceDistance) && isCusp && abs(tEnd.T - t) <= 1 && tEnd.Cusp && (((tEnd.Side & 2) && left) || ((tEnd.Side & 1) && !left))) {
+singleSegmentSpliceCurve[0] = parent[ left ? tEnd.T : t];
+singleSegmentSpliceCurve[2] = parent[!left ? tEnd.T : t];
+singleSegmentSpliceCurve[1] = singleSegmentSpliceCurve[0];
+singleSegmentSpliceCurve[3] = singleSegmentSpliceCurve[2];
+return spliceT;
+}
+
+for (auto startSample = t, endSample = tEnd.T; !((left && startSample < endSample+1e-5) || (!left && startSample > endSample-1e-5)); spliceT = (endSample + startSample)/2)
+{
+if (!_isnan(influenceDistance)) // if influenceDistance has been set, we just use it without subdividing.
+endSample = startSample = spliceT = GetTValueFromSValue(parent, t, tEnd.T, left, influenceDistance, excess);
+
+bool endCusp = spliceT == tEnd.T && tEnd.Cusp && tEnd.Side == 3;
+auto multiSegmentSplitCurve = BezierRep(parent.Split(left ? spliceT : t, left ? t : spliceT) );
+double singleToMultiSegmentError = 0;
+if (multiSegmentSplitCurve.p.size() == 4) { // if split curve is a single-segment bezier, then we it should be 100% accurate
+singleSegmentSpliceCurve[0] = multiSegmentSplitCurve.p[0];
+singleSegmentSpliceCurve[3] = multiSegmentSplitCurve.p[3];
+singleSegmentSpliceCurve[1] = !left && isCusp && isCusp->Side == 3 ? singleSegmentSpliceCurve[0] : multiSegmentSplitCurve.p[1];
+singleSegmentSpliceCurve[2] = left && isCusp && isCusp->Side == 3 ? singleSegmentSpliceCurve[3] : multiSegmentSplitCurve.p[2];
+if (spliceT == endSample)
+break;
+} else {
+const size_t SAMPLES_PER_SEGMENT = 20;
+numMultiSegmentSamples = SampleBezier(multiSegmentSplitCurve, multiSegmentSamplePts, numMultiSegmentSamples, SAMPLES_PER_SEGMENT);
+
+auto endTan = (endSample == tEnd.T && tEnd.Cusp && tEnd.Side == (left ? 1 : 2)) ? parent.Tangent(endSample + (left ? -0.001 : 0.001)) : multiSegmentSplitCurve.Tangent(left ? 0.0 : 1.0*multiSegmentSplitCurve.MaxIndex());
+auto tHat1 = endCusp && left ? Vector2D() : !left ? parTangent : endTan;
+auto tHat2 = endCusp && !left ? Vector2D() : left ? -parTangent : -endTan;
+auto u = BezierRep::ChordLengthParameterize(multiSegmentSamplePts, 0, numMultiSegmentSamples-1);
+GenerateBezier(multiSegmentSamplePts, 0, numMultiSegmentSamples-1, u, tHat1, tHat2, singleSegmentSpliceCurve);
+
+singleToMultiSegmentError = BezierRep::ComputeMaxError(multiSegmentSamplePts, 0, numMultiSegmentSamples-1, singleSegmentSpliceCurve, u);
+}
+if (singleToMultiSegmentError > (endCusp ? 5 : 1) * errorTolerance)
+endSample = spliceT;
+else startSample = spliceT;
+}
+
+if (numMultiSegmentSamples)
+delete [] multiSegmentSamplePts;
+return spliceT;
+}
+
+static void MoveCurveSplice(double t, Point2D splice[4], BezierRep::BezierLock &stepLock, double &extra, bool left, BezierRep::BezierLock *moveLock, const Vector2D &v, double influenceDistance, const Vector2D &smoothParTangent, double ctrlPtScale, double ctrlPtRotate)
+{
+if (!_isnan(influenceDistance) && influenceDistance < 0) {
+splice[left ? 2 : 1] = (splice[left ? 3 : 0] += v);
+if (moveLock) {
+moveLock->Side |= (left ? 1 : 2);
+moveLock->Cusp = true;
+}
+}
+else {
+auto tan = (splice[left?2:1]-splice[left?3:0]);
+splice[left?3:0] += v;
+splice[left?2:1] = splice[left?3:0] + Mat::Rotate(ctrlPtRotate) * tan * ctrlPtScale;
+if (influenceDistance > 0 && t <= stepLock.T ) {
+LnSeg tangent(splice[left?3:0], tan == Vector2D() ? (splice[left?3:0]+smoothParTangent):splice[left?2:1]), otherTangent(splice[left?0:3], splice[left?1:2]);
+auto inter = otherTangent.LnIntersection(tangent);
+auto seglen = (splice[0] - splice[3]).Length();
+if (inter == Point2D::Null()) {
+if (otherTangent.Length() == 0) {
+auto ang = tangent.Direction().UnsignedAngle(splice[left?0:3]-splice[left?3:0]) / M_PI;
+auto target = splice[left?3:0] + tangent.Direction() * .5519 * (ang < 0.01 ? 0 : 1) * seglen;
+
+splice[left?2:1] = (target * std::min(1.0, extra/25) + splice[left?2:1] * std::max(0.0, 1-extra/25));
+}
+} else {
+bool behind = tangent.ClosestFraction(inter) <= 0 && otherTangent.ClosestFraction(inter) <= 0;
+auto tandir = tangent.ClosestFraction(inter) <=0 ? -tangent.Direction() : tangent.Direction();
+auto leglen = std::max(seglen/4, (splice[left?3:0]-inter).Length());
+auto aspect = std::sqrt(leglen / seglen / .7071);
+auto modinter = splice[left?3:0] + tandir * leglen*std::min(1.0,.5519/aspect);
+if (leglen / seglen > 2) {
+if (tangent.Direction().Dot(otherTangent.Direction()) < 0)
+modinter = splice[left?3:0] + tandir * seglen*(.5519);
+else modinter = splice[left?3:0] + tandir * seglen*.7071;
+}
+if (behind && tangent.Direction().Dot(otherTangent.Direction()) < 0)
+modinter = (splice[0] + splice[3])/2;
+auto targetFrac = (modinter-splice[left?3:0]).Length();
+auto target = splice[left?3:0] + targetFrac * tangent.Direction();
+splice[left?2:1] = (target * std::min(1.0, extra/25) + splice[left?2:1] * std::max(0.0, 1-extra/25));
+if (extra> 25) {
+//LnSeg tangent(splice[left?3:0], splice[left?2:1]), otherTangent(splice[left?0:3], splice[left?1:2]);
+auto oextra = extra - 25;
+auto otandir = otherTangent.ClosestFraction(inter) <=0 ? -otherTangent.Direction() : otherTangent.Direction();
+auto oleglen = std::max(seglen/4, (splice[!left?3:0]-inter).Length());
+auto oaspect = std::sqrt(oleglen / seglen / .7071);
+auto omodinter = splice[!left?3:0] + otandir * oleglen*std::min(1.0,.5519/oaspect);
+if (oleglen/ seglen > 2) {
+if (tangent.Direction().Dot(otherTangent.Direction()) < 0)
+omodinter = splice[!left?3:0] + tandir * seglen*(.5519);
+else omodinter = splice[!left?3:0] + otandir * seglen *.7071;
+}
+if (behind && tangent.Direction().Dot(otherTangent.Direction()) < 0)
+omodinter = (splice[0] + splice[3])/2;
+auto otargetFrac = (omodinter-splice[!left?3:0]).Length();
+auto otarget = splice[!left?3:0] + otargetFrac * otherTangent.Direction();
+splice[!left?2:1] = (otarget * std::min(1.0, oextra/25) + splice[!left?2:1] * std::max(0.0, 1-oextra/25));
+}
+}
+}
+}
+}
+static void MoveTAux (BezierRep &curve, double tMove, const Vector2D &v, bool moveEnds)
+{
+auto &p = curve.p;
+auto tstart = static_cast<int>(tMove);
+auto tend = static_cast<int>(ceil(tMove));
+if (tend == tstart)
+{
+if (tend == 0)
+{
+tend = 1;
+}
+else
+{
+tstart = tend - 1;
+}
+}
+auto t = tMove - tstart;
+
+auto b0 = pow(1 - t, 3);
+auto b1 = 3 * t * pow(1 - t, 2);
+auto b2 = 3 * t * t * (1 - t);
+auto b3 = t * t * t;
+
+auto ind = t < 0.4 ? 1 : t > 0.6 ? -1 : 0;
+if (ind == 0) {
+auto norm = (b1 + b2);
+p[tstart * 3 + 1] += (b1/norm * v)/b1;
+p[tend * 3 - 1] += (b2/norm * v)/b2;
+}
+else if (ind == 1 && b1 != 0) {
+auto pt = curve[tMove] + v;
+p[tstart * 3 +1].X = (pt.X - b0 * p[tstart*3].X - b3 * p[tend*3].X - b2 * p[tend*3-1].X) / b1;
+p[tstart * 3 +1].Y = (pt.Y - b0 * p[tstart*3].Y - b3 * p[tend*3].Y - b2 * p[tend*3-1].Y) / b1;
+}
+else if (ind == -1 && b2 != 0) {
+auto pt = curve[tMove] + v;
+p[tend * 3 -1].X = (pt.X - b0 * p[tstart*3].X - b3 * p[tend*3].X - b1 * p[tstart*3+1].X) / b2;
+p[tend * 3 -1].Y = (pt.Y - b0 * p[tstart*3].Y - b3 * p[tend*3].Y - b1 * p[tstart*3+1].Y) / b2;
+}
+
+if (moveEnds) {
+p[tstart * 3] += b0 * v;
+p[tend * 3] += b3 * v;
+}
+
+//p[tstart * 3 + 1] += b1 * v;
+//p[tend * 3 - 1] += b2 * v;
+//p[tstart*3] += v * b0;
+//p[tend*3] += v * b3;
+
+// fx(t):=(1−t)3p1x+3t(1−t)2p2x+3t2(1−t)p3x+t3p4x
+//fy(t):=(1−t)3p1y+3t(1−t)2p2y+3t2(1−t)p3y+t3p4y
+
+//Call the curve C(t) = b0(t) P0 + b1(t) P1 + b2(t) P2 + b3(t) P3. The user clicks at some point Q and drags to a new point R.
+// 3. Compute c0 = b0(s); c1 = b1(s), c2 = b2(s), and c3 = b3(s), the coefficients of the control points at parameter s.
+
+//4. Adjust the Ps like this:
+
+//P0 += c0 * v
+//P1 += c1 * v;
+//P2 += c2 * v;
+//P3 += c3 * v.
+}
+static void MoveTAdaptive (BezierRep &curve, double tMove, const Vector2D &v, std::vector<BezierRep::BezierLock> &locked, bool rangeDrag, double errorTolerance, double influenceLDistance, double influenceRDistance, double ctrlPtLRotate, double ctrlPtRRotate, double ctrlPtScale, bool moveEnds)
+{
+auto tleftMove = rangeDrag ? (static_cast<int>(tMove) == tMove ? tMove-1 : static_cast<int>(tMove)) : tMove;
+auto trightMove = rangeDrag ? (static_cast<int>(tMove) == tMove ? tMove+1 : static_cast<int>(tMove)+1) : tMove;
+auto leftStep = FindSplitIndex(curve,tleftMove, true, locked);
+auto rightStep = FindSplitIndex(curve, trightMove, false, locked);
+auto leftTan = curve.Tangent(std::max(0.0, tleftMove-1e-5));
+auto rightTan = curve.Tangent(std::min(curve.MaxIndex() * 1.0, trightMove + 1e-5));
+auto smoothParTangent = (leftTan + rightTan)/2;
+auto smoothParDist = (curve[std::max(0.0, tMove-1)] - curve[std::min(curve.MaxIndex() * 1.0, tMove+1)]).Length()/4;
+
+BezierRep::BezierLock *isCusp = NULL, *moveLock = NULL;
+for (auto &lck : locked) {
+if (lck.T == tMove) {
+moveLock = &lck;
+if (influenceLDistance > 0 && moveLock && moveLock->Cusp) {
+moveLock->Cusp = false;
+if (moveLock->T*3 - 1 >= 0)
+curve.p[moveLock->T * 3 - 1] = curve.p[moveLock->T * 3] - smoothParTangent*smoothParDist;
+if (moveLock->T*3 + 1 < curve.p.size())
+curve.p[moveLock->T * 3 + 1] = curve.p[moveLock->T * 3] + smoothParTangent*smoothParDist;
+}
+if (lck.Cusp)
+isCusp = &lck;
+break;
+}
+}
+if (moveLock && moveLock->T * 3 -1 >= 0 && moveLock->T*3+1 < curve.p.size() &&
+curve.p[moveLock->T*3-1] == curve.p[moveLock->T*3+1])
+leftTan = rightTan = smoothParTangent;
+// splice the left side of the point that is moved
+Point2D spliceL[4], spliceR[4];
+double lextra=0, rextra= 0;
+auto l = GetSpliceCurve(curve, tleftMove, isCusp, leftStep, locked, v, spliceL, errorTolerance, abs(influenceLDistance), lextra);
+auto r = GetSpliceCurve(curve, trightMove, isCusp, rightStep, locked, v, spliceR, errorTolerance, abs(influenceRDistance), rextra);
+
+BezierRep splicedCurve;
+if (tMove != 0) {
+if (l == -1)
+return;
+
+MoveCurveSplice(l, spliceL, leftStep, lextra, true, moveLock, v, influenceLDistance, -rightTan, ctrlPtScale, ctrlPtLRotate);
+
+// add on the remaining left side of the curve
+if (l != 0)
+splicedCurve = curve.Split(0,l);
+
+// add the spliced left side of the curve
+for (auto i = l == 0 ? 0 : 1; i < 4; i++)
+splicedCurve.p.push_back(spliceL[i]);
+
+if (tleftMove != tMove)
+{
+auto fixedL = curve.Split(tleftMove, tMove);
+for (auto i = 1; i < 4; i++)
+splicedCurve.p.push_back(fixedL[i] + v);
+}
+}
+
+auto moveIndex = splicedCurve.p.size();
+auto insertEnd = moveIndex;
+
+// splice the right side of the point that is moved
+if (tMove != curve.MaxIndex()) {
+if (r == -1)
+return;
+
+if (trightMove != tMove)
+{
+auto fixedL = curve.Split(tMove, trightMove);
+for (auto i = 1; i < 4; i++)
+splicedCurve.p.push_back(fixedL[i] + v);
+}
+MoveCurveSplice(r, spliceR, rightStep, rextra, false, moveLock, v, influenceRDistance, leftTan, ctrlPtScale, ctrlPtRRotate);
+
+// add the spliced right side of the curve
+for (auto i = splicedCurve.p.size() == 0 ? 0 : 1; i < (r != curve.MaxIndex() ? 3 :4); i++) {
+insertEnd++;
+splicedCurve.p.push_back(spliceR[i]);
+}
+if (r != curve.MaxIndex()) {
+insertEnd++;
+for (auto & p : curve.Split(r, 1.0* curve.MaxIndex())) // add on the remaining right side of the curve
+splicedCurve.p.push_back(p);
+}
+}
+
+for (auto & pt : splicedCurve.p) {
+if (_isnan(pt.X))
+break;
+}
+
+// adjust all lock t-values based on the size of the inserted splice segments
+for (auto i = 0; i < locked.size(); i++) {
+if (locked[i].T == tMove)
+locked[i].T = moveIndex ==0 ? 0.0 : (moveIndex*1.0-1)/3;
+else if (locked[i].T == l)
+locked[i].T = std::ceil(l);
+else if (locked[i].T == r)
+locked[i].T = (insertEnd*1.0-1)/3;
+else
+locked[i].T = splicedCurve.NearestT(curve[locked[i].T]);
+}
+curve.p = splicedCurve.p;
+}
+
+
+BezierRep BezierRep::Rotate(const BezierRep &bez, const double angle, const Point2D &center)
+{
+auto rot = Mat::Rotate(angle);
+auto tri = Mat::Translate(-center);
+auto tr = Mat::Translate( center);
+BezierRep moved;
+for (auto &p : bez.p) {
+moved.p.push_back(tr * (rot * (tri *p)));
+}
+return moved;
+}
+BezierRep BezierRep::Move(const BezierRep &bez, const Vector2D &move)
+{
+BezierRep moved;
+for (auto &p : bez.p) {
+moved.p.push_back(p+move);
+}
+return moved;
+}
+BezierRep BezierRep::Interpolate(const BezierRep &start, const BezierRep &end, double t)
+{
+BezierRep interpolated;
+for (auto p=0; p < start.p.size() && p < end.p.size(); p++) {
+interpolated.p.push_back(start.p[p] + (end.p[p]-start.p[p])*t);
+}
+return interpolated;
+}
+std::vector<std::tuple<double, double>> BezierRep::Find_intersections(const BezierRep & a, const BezierRep & b, size_t t_a_off, size_t t_b_off)
+{
+auto ints = std::vector<std::tuple<double, double>>();
+if (a.p.size() == 0 || b.p.size() == 0)
+return ints;
+if (a.p.size() == 4 && b.p.size() == 4)
+{
+std::vector<std::tuple<double, double>> parameters;
+if (SmartRect::Intersect(a.Bounds(), b.Bounds()))
+{
+const int depth = 6;
+Point2D ap[4], bp[4];
+ap[0] = a.p[0];
+ap[1] = a.p[1];
+ap[2] = a.p[2];
+ap[3] = a.p[3];
+bp[0] = b.p[0];
+bp[1] = b.p[1];
+bp[2] = b.p[2];
+bp[3] = b.p[3];
+recursively_intersect(ap, 0, 1, depth, bp, 0, 1, depth, parameters);
+}
+
+std::vector<std::tuple<double, double>> modParameters;
+for (size_t i = 0; i < parameters.size(); i++) {
+modParameters.push_back(std::tuple<double,double>(std::get<0>(parameters[i]) + t_a_off, std::get<1>(parameters[i]) + t_b_off));
+}
+return modParameters;
+}
+for (size_t i = 0; i <= a.p.size() - 4; i += 3)
+{
+for (size_t j = 0; j <= b.p.size() - 4; j += 3)
+{
+std::vector<Point2D> tempVector2(4);
+tempVector2[0] = a.p[i];
+tempVector2[1] = a.p[i + 1];
+tempVector2[2] = a.p[i + 2];
+tempVector2[3] = a.p[i + 3];
+std::vector<Point2D> tempVector3(4);
+tempVector3[0] = b.p[j];
+tempVector3[1] = b.p[j + 1];
+tempVector3[2] = b.p[j + 2];
+tempVector3[3] = b.p[j + 3];
+auto fints = Find_intersections(BezierRep(tempVector2), BezierRep(tempVector3), t_a_off + i / 3, t_b_off + j / 3);
+for (auto inter = 0; inter < fints.size(); inter++) {
+bool newinter = true;
+for (auto & oint : ints)
+if (std::get<0>(oint) == std::get<0>(fints[inter]) &&
+std::get<1>(oint) == std::get<1>(fints[inter])) {
+newinter = false;
+break;
+}
+if (newinter)
+ints.push_back(fints[inter]);
+}
+}
+}
+return ints;
+}
+std::vector<std::vector<Point2D> > BezierRep::FitCurveSet( const Point2D d[], size_t dSize, double error, bool & isLoop) {
+std::vector<std::vector<Point2D>> fitSet;
+fitSet.push_back(::FitCurve(d, dSize, error));
+return fitSet;
+}
+std::vector<Point2D> BezierRep::FitCurve( const std::vector<Point2D> &d, double error)
+{
+return ::FitCurve(d.data(), d.size(), error);
+}
+std::vector<Point2D> BezierRep::FitOneCurve(const std::vector<Point2D> &d)
+{
+return::FitOneCurve(d.data(), d.size());
+}
+
+std::vector<double> BezierRep::Reparameterize( const Point2D d[], size_t first, size_t last, const std::vector<double> &u, const Point2D bezCurve[4])
+{
+std::vector<double> uPrime(last - first + 1); // New parameter values
+
+for (auto i = first; i <= last; i++)
+{
+uPrime[i - first] = NewtonRaphsonRootFind(bezCurve, d[i], u[i - first]);
+}
+return uPrime;
+}
+double BezierRep::ComputeMaxError(const Point2D d[], size_t first, size_t last, const Point2D bezCurve[4], const std::vector<double> &u, size_t *splitPoint2D)
+{
+double maxDist; // Maximum error
+
+if (splitPoint2D)
+*splitPoint2D = (last - first + 1) / 2;
+maxDist = 0.0;
+for (auto i = first + 1; i < last; i++)
+{
+ double P[2]; // point on curve
+EvalBezierFast(bezCurve, u[i-first], P);
+double dx = P[0] - d[i].X;// offset from point to curve
+double dy = P[1] - d[i].Y;
+auto dist = sqrt(dx*dx+dy*dy); // Current error
+if (dist >= maxDist)
+{
+maxDist = dist;
+if (splitPoint2D)
+*splitPoint2D = i;
+}
+}
+return maxDist;
+}
+std::vector<double> BezierRep::ChordLengthParameterize(const Point2D d[], size_t first, size_t last)
+{
+std::vector<double> u(last-first+1);// Parameterization
+
+double prev = 0.0;
+u[0] = prev;
+for (auto i = first + 1; i <= last; i++)
+{
+auto & lastd = d[i-1];
+auto & curd = d[i];
+auto dx = lastd.X - curd.X;
+auto dy = lastd.Y - curd.Y;
+prev = u[i - first] = prev + sqrt(dx*dx+dy*dy);
+}
+
+double ulastfirst = u[last-first];
+for (auto i = first + 1; i <= last; i++)
+{
+u[i - first] /= ulastfirst;
+}
+
+return u;
+}
+
+void BezierRep::InsertCpt(double tstart)
+{ auto &allPts = p;
+ auto t_start_base = (size_t)tstart;
+ if (t_start_base >= MaxIndex())
+ t_start_base = MaxIndex() - 1;
+
+ Point2D left[4], right[4];
+ splitCubic(&allPts[t_start_base*3], tstart - t_start_base, left, right);
+std::vector<Point2D> newP;
+for (size_t i = 0; i < t_start_base*3; i++)
+newP.push_back(allPts[i]);
+for (size_t i = 0; i < 4; i++)
+newP.push_back(left[i]);
+for (size_t i = 1; i < 4; i++)
+newP.push_back(right[i]);
+for (size_t i = t_start_base*3+4; i < allPts.size(); i++)
+newP.push_back(allPts[i]);
+p = newP;
+}
+std::vector<Point2D> BezierRep::Split(double tstart, double tend) const
+{
+ auto t_start_base = static_cast<size_t>(tstart);
+ auto t_end_base = static_cast<size_t>(tend);
+ auto maxIndex = MaxIndex();
+ if (t_start_base >= maxIndex)
+ t_start_base = maxIndex - 1;
+ if (t_end_base >= maxIndex)
+ t_end_base = maxIndex - 1;
+
+ Point2D split[4];
+ std::vector<Point2D> splitPts(4);
+ if (t_start_base != t_end_base)
+ {
+bool used4 = true;
+if (tstart - t_start_base == 0) {
+splitPts[0] = p[t_start_base*3];
+splitPts[1] = p[t_start_base*3+1];
+splitPts[2] = p[t_start_base*3+2];
+splitPts[3] = p[t_start_base*3+3];
+} else {
+splitCubic(&(p[t_start_base*3]), tstart - t_start_base, NULL, split);
+if (!(split[0].X == split[1].X && split[1].X == split[2].X && split[2].X == split[3].X &&
+ split[0].Y == split[1].Y && split[1].Y == split[2].Y && split[2].Y == split[3].Y))
+for (size_t i = 0; i < 4; i++)
+splitPts[i] = split[i];
+else {
+splitPts[0] = split[0];
+used4 = false;
+}
+}
+ for (auto i = (t_start_base + 1) * 3; i < t_end_base * 3; i += 3) {
+if (!used4) {
+used4 = true;
+splitPts[1] = p[i+1];
+splitPts[2] = p[i+2];
+splitPts[3] = p[i+3];
+} else {
+splitPts.push_back(p[i+1]);
+splitPts.push_back(p[i+2]);
+splitPts.push_back(p[i+3]);
+}
+}
+ if (t_end_base * 3 < p.size() - 1 && tend - t_end_base != 0)
+ {
+splitCubic(&(p[t_end_base *3]), tend - t_end_base, split, NULL);
+if (!(split[0].X == split[1].X && split[1].X == split[2].X && split[2].X == split[3].X &&
+split[0].Y == split[1].Y && split[1].Y == split[2].Y && split[2].Y == split[3].Y))
+{
+if (!used4) {
+splitPts[1] = split[1];
+splitPts[2] = split[2];
+splitPts[3] = split[3];
+} else {
+splitPts.push_back(split[1]);
+splitPts.push_back(split[2]);
+splitPts.push_back(split[3]);
+}
+
+}
+ }
+ }
+ else
+ {
+Point2D tmp[4];
+splitCubic(&(p[t_end_base *3]), tend-t_end_base, tmp, NULL);
+splitCubic(tmp, tstart==tend ? 0 : (tstart-t_end_base) / (tend-t_end_base), NULL, split);
+for (auto i = 0; i < 4; i++)
+splitPts[i] = split[i];
+ }
+return splitPts;
+}
+void BezierRep::MoveT(double tMove, const Vector2D & v, bool moveEnds, std::vector<BezierLock> &locked, bool adaptive, bool rangeDrag, double errorTolerance, double influenceLDistance, double influenceRDistance, double ctrlPtLRotate, double ctrlPtRRotate, double ctrlPtScale) {
+if (adaptive)
+MoveTAdaptive(*this, tMove, v, locked, rangeDrag, errorTolerance, influenceLDistance, influenceRDistance, ctrlPtLRotate, ctrlPtRRotate, ctrlPtScale, moveEnds);
+else MoveTAux(*this, tMove, v, moveEnds);
+}
+void BezierRep::GetPoint(const std::vector<Point2D> &p, double t, Point2D &result)
+{
+while (t < 0) {
+t += (p.size()-1)/3;
+}
+while (t > (p.size()-1)/3) {
+t -= (p.size()-1)/3;
+}
+if (p.size() == 0)
+return;
+size_t t_base = 0;
+if (p.size() > 4)
+{
+t_base = static_cast<size_t>(t);
+if (t_base * 3 + 1 >= p.size() - 2) {
+result.X = p.back().X;
+result.Y = p.back().Y;
+return;
+}
+t = t- t_base;
+}
+
+Point2D bez[4] = { p[t_base * 3 + 0], p[t_base * 3 + 1], p[t_base * 3 + 2], p[t_base * 3 + 3] };
+double res[2];
+EvalBezierFast(bez, t, res);
+result.X = res[0];
+result.Y = res[1];
+}
+double BezierRep::NearestT(const Point2D &Pt) const
+{
+if (p.size() < 1)
+return 0;
+
+double closest = DBL_MAX;
+double tclosest = -1;
+for (size_t i = 0; i< MaxIndex(); i++) {
+std::vector<Point2D> tmppts;
+tmppts.push_back(p[i*3]);
+tmppts.push_back(p[i*3+1]);
+tmppts.push_back(p[i*3+2]);
+tmppts.push_back(p[i*3+3]);
+double tc;
+auto nrst = NearestPointOnCurve(Pt, tmppts, &tc);
+if ((nrst-Pt).Length() < closest) {
+closest = (nrst-Pt).Length();
+tclosest = tc+i;
+}
+}
+return tclosest;
+}
+Vector2D BezierRep::Tangent(double T) const
+{
+while (T < 0) {
+T += (p.size()-1)/3;
+}
+while (T > (p.size()-1)/3) {
+T -= (p.size()-1)/3;
+}
+if (T == 0)
+{
+for (auto i = 1; i < p.size(); i++)
+if (p[i] != p[0])
+return (p[i]-p[0]).Normal();
+//else return Vector2D();
+return Vector2D();
+}
+if (T == MaxIndex())
+{
+for (int i = static_cast<int>(p.size())-2; i >= 0; i--)
+if (p[i] != p.back())
+return (p.back()-p[i]).Normal();
+//else return Vector2D();
+return Vector2D();
+}
+
+
+int segStart = 3 * (static_cast<int>(T));
+auto t = T - static_cast<int>(T);
+auto A = p[segStart] - p[segStart];
+auto B = p[segStart + 1] - p[segStart];
+// if (B == Vector2D() && segStart > 0 && (p[segStart-1] - p[segStart]) == Vector2D())
+// return Vector2D();
+auto C = p[segStart + 2] - p[segStart];
+auto D = p[segStart + 3] - p[segStart];
+// note that abcd are aka x0 x1 x2 x3
+
+auto tan = -3*A*(1-t)*(1-t) + B*(3*(1-t)*(1-t) - 6*(1 - t)*t) + C*(6*(1 - t)*t - 3*t*t) + 3*D*t*t;
+return tan.Normal();
+
+// the four coefficients ..
+// A = x3 - 3 * x2 + 3 * x1 - x0
+// B = 3 * x2 - 6 * x1 + 3 * x0
+// C = 3 * x1 - 3 * x0
+// D = x0
+//
+// and then...
+// Vx = 3At2 + 2Bt + C
+
+// first calcuate what are usually know as the coeffients,
+// they are trivial based on the four control points:
+
+//double C1x = (D.X - (3.0 * C.X) + (3.0 * B.X) - A.X);
+//double C2x = ((3.0 * C.X) - (6.0 * B.X) + (3.0 * A.X));
+//double C3x = ((3.0 * B.X) - (3.0 * A.X));
+//double C4x = (A.X); // (not needed for this calculation)
+
+//double C1y = (D.Y - (3.0 * C.Y) + (3.0 * B.Y) - A.Y);
+//double C2y = ((3.0 * C.Y) - (6.0 * B.Y) + (3.0 * A.Y));
+//double C3y = ((3.0 * B.Y) - (3.0 * A.Y));
+//double C4y = (A.Y); // (not needed for this calculation)
+
+// finally it is easy to calculate the slope element, using those coefficients:
+
+//Vector2D vec(((3.0 * C1x * t * t) + (2.0 * C2x * t) + C3x), ((3.0 * C1y * t * t) + (2.0 * C2y * t) + C3y));
+
+//vec.Normalize();
+//return vec;
+// note that this routine works for both the x and y side;
+// simply run this routine twice, once for x once for y
+// note that there are sometimes said to be 8 (not 4) coefficients,
+// these are simply the four for x and four for y, calculated as above in each case.
+}
+bool BezierRep::IsDiscontinuity(int t) const
+{
+if (t == 0 || t == MaxIndex()) {
+if (p.front() != p.back())
+return true;
+
+auto inTan = (p[1]-p[0]).Normal();
+auto outTan = (p[p.size()-2]-p[p.size()-1]).Normal();
+if (_isnan(inTan.X) || _isnan(outTan.X) || inTan.Dot(outTan) > -0.998)
+return true;
+}
+
+return false;
+}
+Point2D BezierRep::Reflect(const Point2D &srcPt) const
+{
+auto nrstT = NearestT(srcPt);
+auto nrst = (*this)[nrstT];
+if (nrstT < 1e-4)
+nrstT = 0;
+if ((MaxIndex()-nrstT) < 1e-4)
+nrstT = static_cast<double>(MaxIndex());
+if (nrstT == 0 || nrstT == MaxIndex() || (p.size()== 4 && p[0]==p[1] && p[2]==p[3])) {
+LnSeg seg(nrst, Tangent(nrstT));
+nrst = seg.LnClosestPoint(srcPt);
+}
+auto normal = Normal(nrstT);
+auto offset = (nrst - srcPt).Length();
+if (normal.Dot(srcPt-nrst) > 0)
+normal = -normal;
+return nrst + normal * offset;
+}
+BezierRep BezierRep::Reflect(const BezierRep &b) const {
+std::vector<Point2D> reflected;
+for (auto &p : b.p) {
+reflected.push_back(Reflect(p));
+}
+return BezierRep(reflected);
+}
+
+//
+// ReflectAndClip - Clips one curve against another, then reflects the clipped segments.
+// This returns two lists of reflected segments corresponding to reflections of segments which were on the same side as the
+// initial point of the stroke (relative to the reflection axis) and those which which were on the opposite side.
+//
+std::vector<std::vector<std::tuple<BezierRep,BezierRep>>> BezierRep::ReflectAndClip(const BezierRep &b) const
+{
+BezierRep testRep = *this;
+if (MaxIndex() == 1 && p[0]==p[1] && p[2]==p[3]) {
+Vector2D dir = p[3]-p[0];
+std::vector<Point2D> pts;
+pts.push_back(p[0] - 10000 * dir);
+pts.push_back(p[0] - 10000 * dir);
+pts.push_back(p[3] + 10000 * dir);
+pts.push_back(p[3] + 10000 * dir);
+testRep = BezierRep(pts);
+}
+auto ints = Find_intersections(testRep, b);
+
+std::vector<std::vector<BezierRep>> flipSets;
+std::vector<BezierRep> fragments[2];
+if (ints.size() == 0) {
+fragments[0].push_back(b);
+flipSets.push_back(fragments[0]);
+} else {
+double start = 0;
+int which = 0;
+for (auto &i: ints) {
+auto split = b.Split(start, std::get<1>(i));
+fragments[which++%2].push_back(split);
+start = std::get<1>(i);
+}
+fragments[which++%2].push_back(b.Split(start, static_cast<double>(b.MaxIndex())));
+
+}
+
+std::vector<std::vector<std::tuple<BezierRep,BezierRep>>> mirroredSides;
+for (auto &side: fragments) {
+std::vector<std::tuple<BezierRep,BezierRep>> mirrors;
+for (auto &f : side)
+mirrors.push_back(std::tuple<BezierRep,BezierRep>(f, Reflect(f)));
+mirroredSides.push_back(mirrors);
+}
+return mirroredSides;
+}
+#ifdef later
+if (!_isnan(influenceDistance) && influenceDistance < 0) {
+spliceL[2] = (spliceL[3] += v);
+if (moveLock) {
+moveLock->Side |= 1;
+moveLock->Cusp = true;
+}
+}
+else if (influenceDistance > 0 && l <= leftStep.T && spliceL[2] != spliceL[3]) {
+auto lTan = (spliceL[2]-spliceL[3]);
+spliceL[2] = spliceL[3] + Mat::Rotate(ctrlPtRotate) * lTan * ctrlPtScale + v;
+spliceL[3] += v;
+
+LnSeg tangent(spliceL[3], spliceL[2]), otherTangent(spliceL[0], spliceL[1]);
+auto inter = otherTangent.LnIntersection(tangent);
+if (inter != Point2D::Null()) {
+auto tandir = tangent.ClosestFraction(inter) <=0 ? -tangent.Direction() : tangent.Direction();
+auto aspect = (spliceL[3]-inter).Length() / (spliceL[0]-spliceL[3]).Length() / .7071;
+auto modinter = spliceL[3] + tandir * (spliceL[3]-inter).Length()*std::min(1.0,.5519/aspect);
+
+auto targetFrac = (modinter-spliceL[3]).Length();
+auto target = spliceL[3] + targetFrac * tangent.Direction();
+spliceL[2] = (target * std::min(1.0, lextra/25) + spliceL[2] * std::max(0.0, 1-lextra/25));
+}
+} else {
+auto lTan = (spliceL[2]-spliceL[3]);
+if (lTan == Vector2D() && influenceDistance > 0) {
+if (moveLock)
+moveLock->Cusp = false;
+spliceL[2] = spliceL[3] + v - smoothParTangent.Normal()*lextra;
+} else
+spliceL[2] = spliceL[3] + Mat::Rotate(ctrlPtRotate) * lTan * ctrlPtScale + v;
+spliceL[3] += v;
+}
+#endif
+#if 0
+if (!_isnan(influenceDistance) && influenceDistance < 0) {
+spliceR[1] = (spliceR[0] += v);
+if (moveLock) {
+moveLock->Side |= 2;
+moveLock->Cusp = true;
+}
+}
+else
+if (influenceDistance > 0 && r>=rightStep.T && spliceR[1] != spliceR[0]) {
+
+auto rTan = (spliceR[1]-spliceR[0]);
+spliceR[1] = spliceR[0] + Mat::Rotate(ctrlPtRotate) * rTan* ctrlPtScale + v;
+spliceR[0] += v;
+
+LnSeg tangent(spliceR[0], spliceR[1]), otherTangent(spliceR[3], spliceR[2]);
+auto inter = otherTangent.LnIntersection(tangent);
+if (inter != Point2D::Null()) {
+auto tandir = tangent.ClosestFraction(inter) <=0 ? -tangent.Direction() : tangent.Direction();
+//auto aspect = (spliceR[0]-inter).Length() / (spliceR[3]-inter).Length();
+auto aspect = (spliceR[0]-inter).Length() / (spliceR[0]-spliceR[3]).Length() / .7071;
+auto modinter = spliceR[0] + tandir * (spliceR[0]-inter).Length()*std::min(1.0,.5519/aspect);
+
+auto targetFrac = (modinter-spliceR[0]).Length();
+auto target = spliceR[0] + targetFrac * tangent.Direction();
+spliceR[1] = (target * std::min(1.0, rextra/25) + spliceR[1] * std::max(0.0, 1-rextra/25));
+}
+} else {
+auto rTan = (spliceR[1]-spliceR[0]);
+if (rTan == Vector2D() && influenceDistance > 0) {
+if (moveLock)
+moveLock->Cusp = false;
+spliceR[1] = spliceR[0] + v + smoothParTangent.Normal()*rextra;
+} else
+spliceR[1] = spliceR[0] + Mat::Rotate(ctrlPtRotate) * rTan* ctrlPtScale + v;
+spliceR[0] += v;
+}
+#endif
+
+*/ \ No newline at end of file
diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx
index 32c351bf5..b9772fd57 100644
--- a/src/client/views/DocComponent.tsx
+++ b/src/client/views/DocComponent.tsx
@@ -44,7 +44,7 @@ interface ViewBoxBaseProps {
fieldKey: string;
layerProvider?: (doc: Doc, assign?: boolean) => boolean;
isSelected: (outsideReaction?: boolean) => boolean;
- isContentActive: () => boolean;
+ isContentActive: () => boolean | undefined;
renderDepth: number;
rootSelected: (outsideReaction?: boolean) => boolean;
}
@@ -65,10 +65,12 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps, T>(schemaCtor:
lookupField = (field: string) => ScriptCast(this.layoutDoc.lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field, container: this.props.ContainingCollectionDoc }).result;
- isContentActive = (outsideReaction?: boolean) => (CurrentUserUtils.SelectedTool !== InkTool.None ||
- (this.props.isContentActive?.() || this.props.Document.forceActive ||
- this.props.isSelected(outsideReaction) ||
- this.props.rootSelected(outsideReaction)) ? true : false)
+ isContentActive = (outsideReaction?: boolean) => (
+ this.props.isContentActive?.() === false ? false :
+ (CurrentUserUtils.SelectedTool !== InkTool.None ||
+ (this.props.isContentActive?.() || this.props.Document.forceActive ||
+ this.props.isSelected(outsideReaction) ||
+ this.props.rootSelected(outsideReaction)) ? true : undefined))
protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer;
}
return Component;
@@ -82,7 +84,7 @@ export interface ViewBoxAnnotatableProps {
fieldKey: string;
filterAddDocument?: (doc: Doc[]) => boolean; // allows a document that renders a Collection view to filter or modify any documents added to the collection (see PresBox for an example)
layerProvider?: (doc: Doc, assign?: boolean) => boolean;
- isContentActive: () => boolean;
+ isContentActive: () => boolean | undefined;
select: (isCtrlPressed: boolean) => void;
whenChildContentsActiveChanged: (isActive: boolean) => void;
isSelected: (outsideReaction?: boolean) => boolean;
@@ -165,13 +167,13 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps, T
// otherwise, the document being moved must be able to be removed from its container before
// moving it into the target.
@action.bound
- moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean, annotationKey?: string): boolean => {
+ moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[], annotationKey?: string) => boolean, annotationKey?: string): boolean => {
if (Doc.AreProtosEqual(this.props.Document, targetCollection)) {
return true;
}
const first = doc instanceof Doc ? doc : doc[0];
if (!first?._stayInCollection && addDocument !== returnFalse) {
- return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc));
+ return UndoManager.RunInTempBatch(() => this.removeDocument(doc, annotationKey, true) && addDocument(doc, annotationKey));
}
return false;
}
diff --git a/src/client/views/DocumentDecorations.scss b/src/client/views/DocumentDecorations.scss
index d8ad47ecb..82dca1287 100644
--- a/src/client/views/DocumentDecorations.scss
+++ b/src/client/views/DocumentDecorations.scss
@@ -240,8 +240,10 @@ $linkGap: 3px;
text-align: center;
display: flex;
margin-left: 5px;
- height: 22px;
+ height: 20px;
position: absolute;
+ border-radius: 8px;
+ background: rgba(159,159,159,0.1);
.documentDecorations-titleSpan,
.documentDecorations-titleSpan-Dark {
@@ -288,7 +290,7 @@ $linkGap: 3px;
text-align: center;
display: flex;
margin-left: 5px;
- height: 22px;
+ height: 20px;
position: absolute;
.documentDecorations-titleSpan {
width: 100%;
diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx
index 522995479..d85709f31 100644
--- a/src/client/views/DocumentDecorations.tsx
+++ b/src/client/views/DocumentDecorations.tsx
@@ -27,6 +27,8 @@ import { LightboxView } from './LightboxView';
import { DocumentView } from "./nodes/DocumentView";
import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox';
import React = require("react");
+import { InkingStroke } from './InkingStroke';
+import e = require('express');
@observer
export class DocumentDecorations extends React.Component<{ PanelWidth: number, PanelHeight: number, boundsLeft: number, boundsTop: number }, { value: string }> {
@@ -37,12 +39,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
private _linkBoxHeight = 20 + 3; // link button height + margin
private _titleHeight = 20;
private _resizeUndo?: UndoManager.Batch;
- private _rotateUndo?: UndoManager.Batch;
private _offX = 0; _offY = 0; // offset from click pt to inner edge of resize border
private _snapX = 0; _snapY = 0; // last snapped location of resize border
- private _prevY = 0;
private _dragHeights = new Map<Doc, { start: number, lowest: number }>();
- private _inkCenterPts: { doc: Doc, X: number, Y: number }[] = [];
private _inkDragDocs: { doc: Doc, x: number, y: number, width: number, height: number }[] = [];
@observable private _accumulatedTitle = "";
@@ -194,30 +193,22 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
@action
onRotateDown = (e: React.PointerEvent): void => {
- this._rotateUndo = UndoManager.StartBatch("rotatedown");
- const pt = { x: this.Bounds.c?.X ?? (this.Bounds.x + this.Bounds.r) / 2, y: this.Bounds.c?.Y ?? (this.Bounds.y + this.Bounds.b) / 2 };
+ const rotateUndo = UndoManager.StartBatch("rotatedown");
+ const centerPoint = { X: this.Bounds.c?.X ?? (this.Bounds.x + this.Bounds.r) / 2, Y: this.Bounds.c?.Y ?? (this.Bounds.y + this.Bounds.b) / 2 };
setupMoveUpEvents(this, e,
(e: PointerEvent, down: number[], delta: number[]) => {
- const docView = SelectionManager.Views()[0];
- const { left, top, right, bottom } = docView.getBounds() || { left: 0, top: 0, right: 0, bottom: 0 };
- const centerPoint = { X: (left + right) / 2, Y: (top + bottom) / 2 };
const previousPoint = { X: e.clientX, Y: e.clientY };
const movedPoint = { X: e.clientX - delta[0], Y: e.clientY - delta[1] };
- const angle = InkStrokeProperties.Instance?.angleChange(previousPoint, movedPoint, centerPoint);
- const selectedInk = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK);
- angle && InkStrokeProperties.Instance?.rotateInk(selectedInk, -angle, pt);
+ const angle = InkStrokeProperties.angleChange(previousPoint, movedPoint, centerPoint);
+ const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke);
+ angle && InkStrokeProperties.Instance.rotateInk(selectedInk, -angle, centerPoint);
return false;
},
() => {
- this._rotateUndo?.end();
+ rotateUndo?.end();
UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
},
emptyFunction);
- this._prevY = e.clientY;
- this._inkCenterPts = SelectionManager.Views()
- .filter(dv => dv.rootDoc.type === DocumentType.INK)
- .map(dv => ({ ink: Cast(dv.rootDoc.data, InkField)?.inkData ?? [{ X: 0, Y: 0 }], doc: dv.rootDoc }))
- .map(({ ink, doc }) => ({ doc, X: Math.min(...ink.map(p => p.X)), Y: Math.min(...ink.map(p => p.Y)) }));
}
@action
@@ -226,7 +217,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
this._inkDragDocs = DragManager.docsBeingDragged
.filter(doc => doc.type === DocumentType.INK)
.map(doc => {
- if (InkStrokeProperties.Instance?._lock) {
+ if (InkStrokeProperties.Instance._lock) {
Doc.SetNativeHeight(doc, NumCast(doc._height));
Doc.SetNativeWidth(doc, NumCast(doc._width));
}
@@ -249,7 +240,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
const first = SelectionManager.Views()[0];
let thisPt = { x: e.clientX - this._offX, y: e.clientY - this._offY };
var fixedAspect = Doc.NativeAspect(first.layoutDoc);
- InkStrokeProperties.Instance?._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK)
+ InkStrokeProperties.Instance._lock && SelectionManager.Views().filter(dv => dv.rootDoc.type === DocumentType.INK)
.forEach(dv => fixedAspect = Doc.NativeAspect(dv.rootDoc));
const resizeHdl = this._resizeHdlId.split(" ")[0];
@@ -454,17 +445,18 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
</Tooltip>);
const colorScheme = StrCast(CurrentUserUtils.ActiveDashboard?.colorScheme);
- const titleArea = this._edtingTitle ?
- <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`}
- style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }}
- type="text" name="dynbox" autoComplete="on"
- value={this._accumulatedTitle}
- onBlur={e => this.titleBlur()}
- onChange={action(e => this._accumulatedTitle = e.target.value)}
- onKeyPress={this.titleEntered} /> :
- <div className="documentDecorations-title" style={{ width: `calc(100% - ${seldoc?.props.hideResizeHandles ? 0 : 20}px` }} key="title" onPointerDown={this.onTitleDown} >
- <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span>
- </div>;
+ const titleArea = hideTitle ? <div className="documentDecorations-title" onPointerDown={this.onTitleDown} style={{ width: "100%" }} key="title" /> :
+ this._edtingTitle ?
+ <input ref={this._keyinput} className={`documentDecorations-title${colorScheme}`}
+ style={{ width: `calc(100% - ${hideResizers ? 0 : 20}px` }}
+ type="text" name="dynbox" autoComplete="on"
+ value={this._accumulatedTitle}
+ onBlur={e => this.titleBlur()}
+ onChange={action(e => this._accumulatedTitle = e.target.value)}
+ onKeyPress={this.titleEntered} /> :
+ <div className="documentDecorations-title" style={{ width: `calc(100% - ${hideResizers ? 0 : 20}px` }} key="title" onPointerDown={this.onTitleDown} >
+ <span className={`documentDecorations-titleSpan${colorScheme}`}>{`${this.selectionTitle}`}</span>
+ </div>;
let inMainMenuPanel = false;
for (let node = seldoc.ContentDiv; node && !inMainMenuPanel; node = node?.parentNode as any) {
@@ -478,7 +470,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
bounds.r = Math.max(bounds.x, Math.max(leftBounds, Math.min(window.innerWidth, bounds.r + borderRadiusDraggerWidth + this._resizeBorderWidth / 2) - this._resizeBorderWidth / 2 - borderRadiusDraggerWidth));
bounds.b = Math.max(bounds.y, Math.max(topBounds, Math.min(window.innerHeight, bounds.b + this._resizeBorderWidth / 2 + this._linkBoxHeight) - this._resizeBorderWidth / 2 - this._linkBoxHeight));
- const useRotation = seldoc.rootDoc.type === DocumentType.INK;
+ const useRotation = seldoc.ComponentView instanceof InkingStroke;
const resizerScheme = colorScheme ? "documentDecorations-resizer" + colorScheme : "";
return (<div className={`documentDecorations${colorScheme}`}>
@@ -498,8 +490,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
top: bounds.y - this._resizeBorderWidth / 2 - this._titleHeight,
}}>
{!canDelete ? <div /> : topBtn("close", "times", undefined, this.onCloseClick, "Close")}
- {hideTitle ? (null) : titleArea}{!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")}
-
+ {titleArea}
+ {!canOpen ? (null) : topBtn("open", "external-link-alt", this.onMaximizeDown, undefined, "Open in Tab (ctrl: as alias, shift: in new collection)")}
{hideResizers ? (null) :
<>
{SelectionManager.Views().length !== 1 || hideTitle ? (null) :
@@ -517,7 +509,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number, P
{seldoc.props.renderDepth <= 1 || !seldoc.props.ContainingCollectionView ? (null) :
topBtn("selector", "arrow-alt-circle-up", undefined, this.onSelectorClick, "tap to select containing document")}
<div key="rot" className={`documentDecorations-${useRotation ? "rotation" : "borderRadius"}`}
- onPointerDown={useRotation ? this.onRotateDown : this.onRadiusDown} onContextMenu={(e) => e.preventDefault()}>{useRotation && "⟲"}</div>
+ onPointerDown={useRotation ? this.onRotateDown : this.onRadiusDown}
+ onContextMenu={e => e.preventDefault()}>{useRotation && "⟲"}</div>
</>
}
</div >
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index f28485e43..04abdbf37 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -18,7 +18,7 @@ import { SelectionManager } from "../util/SelectionManager";
import { Transform } from "../util/Transform";
import { CollectionFreeFormViewChrome } from "./collections/CollectionMenu";
import "./GestureOverlay.scss";
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, SetActiveArrowStart, SetActiveDash, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "./InkingStroke";
+import { ActiveArrowEnd, ActiveArrowStart, ActiveArrowScale, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, SetActiveArrowStart, SetActiveDash, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "./InkingStroke";
import { DocumentView } from "./nodes/DocumentView";
import { RadialMenu } from "./nodes/RadialMenu";
import HorizontalPalette from "./Palette";
@@ -850,14 +850,14 @@ export class GestureOverlay extends Touchable {
const b = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 };//this.getBounds(l, true);
return <svg key={i} width={b.width} height={b.height} style={{ transform: `translate(${b.left}px, ${b.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
{InteractionUtils.CreatePolyline(l, b.left, b.top, strokeColor, width, width, "miter", "round",
- ActiveInkBezierApprox(), "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(),
+ ActiveInkBezierApprox(), "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(), ActiveArrowScale(),
ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)}
</svg>;
}),
this._points.length <= 1 ? (null) : <svg key="svg" width={B.width} height={B.height}
style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}>
{InteractionUtils.CreatePolyline(this._points.map(p => ({ X: p.X, Y: p.Y - (rect?.y || 0) })), B.left, B.top, ActiveInkColor(), width, width, "miter", "round", "",
- "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)}
+ "none" /*ActiveFillColor()*/, ActiveArrowStart(), ActiveArrowEnd(), ActiveArrowScale(), ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)}
</svg>]
];
}
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index 364bf05e2..1a4080d81 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -112,7 +112,7 @@ export class KeyManager {
case "escape":
DocumentLinksButton.StartLink = undefined;
DocumentLinksButton.StartLinkView = undefined;
- InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlButton = false);
+ InkStrokeProperties.Instance._controlButton = false;
CurrentUserUtils.SelectedTool = InkTool.None;
var doDeselect = true;
if (SnappingManager.GetIsDragging()) {
@@ -230,6 +230,10 @@ export class KeyManager {
}
}
break;
+ case "e": CurrentUserUtils.SelectedTool = InkTool.Eraser;
+ break;
+ case "p": CurrentUserUtils.SelectedTool = InkTool.Pen;
+ break;
case "o":
const target = SelectionManager.Docs().lastElement();
target && CollectionDockingView.OpenFullScreen(target);
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index f24dab949..24f796105 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -6,13 +6,14 @@ import { ControlPoint, InkData, PointData, InkField } from "../../fields/InkFiel
import { List } from "../../fields/List";
import { listSpec } from "../../fields/Schema";
import { Cast, NumCast } from "../../fields/Types";
-import { setupMoveUpEvents } from "../../Utils";
+import { setupMoveUpEvents, returnFalse } from "../../Utils";
import { Transform } from "../util/Transform";
import { UndoManager } from "../util/UndoManager";
import { Colors } from "./global/globalEnums";
import { InkingStroke } from "./InkingStroke";
import { InkStrokeProperties } from "./InkStrokeProperties";
import { DocumentView } from "./nodes/DocumentView";
+import { SelectionManager } from "../util/SelectionManager";
export interface InkControlProps {
inkDoc: Doc;
@@ -44,23 +45,24 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
@action
onControlDown = (e: React.PointerEvent, controlIndex: number): void => {
const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen;
- if (InkStrokeProperties.Instance && ptFromScreen) {
+ if (ptFromScreen) {
const order = controlIndex % 4;
const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length;
const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length;
const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"));
- const wasSelected = InkStrokeProperties.Instance?._currentPoint === controlIndex;
+ const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
+ const origInk = this.props.inkCtrlPoints.slice();
setupMoveUpEvents(this, e,
action((e: PointerEvent, down: number[], delta: number[]) => {
if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] });
const inkMoveStart = ptFromScreen({ X: 0, Y: 0 });
- InkStrokeProperties.Instance?.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
+ InkStrokeProperties.Instance.moveControlPtHandle(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex, origInk);
return false;
}),
action(() => {
if (this.controlUndo) {
- InkStrokeProperties.Instance?.snapControl(this.props.inkView, controlIndex);
+ InkStrokeProperties.Instance.snapControl(this.props.inkView, controlIndex);
}
this.controlUndo?.end();
this.controlUndo = undefined;
@@ -75,11 +77,11 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
} else {
if (brokenIndices?.includes(equivIndex)) {
if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
- InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB);
+ InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB);
}
if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) {
if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth");
- InkStrokeProperties.Instance?.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB);
+ InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB);
}
}
this.controlUndo?.end();
@@ -102,7 +104,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
@action
onDelete = (e: KeyboardEvent) => {
if (["-", "Backspace", "Delete"].includes(e.key)) {
- InkStrokeProperties.Instance?.deletePoints(this.props.inkView);
+ InkStrokeProperties.Instance.deletePoints(this.props.inkView, e.shiftKey);
e.stopPropagation();
}
}
@@ -111,11 +113,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
* Changes the current selected control point.
*/
@action
- changeCurrPoint = (i: number) => {
- if (InkStrokeProperties.Instance) {
- InkStrokeProperties.Instance._currentPoint = i;
- }
- }
+ changeCurrPoint = (i: number) => InkStrokeProperties.Instance._currentPoint = i
render() {
// Accessing the current ink's data and extracting all control points.
@@ -133,7 +131,6 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
inkCtrlPts.push({ ...inkData[i + 3], I: i + 3 });
}
- const screenSpaceLineWidth = this.props.screenSpaceLineWidth;
const closed = InkingStroke.IsClosed(inkData);
const nearestScreenPt = this.props.nearestScreenPt();
const TagType = (broken?: boolean) => broken ? "rect" : "circle";
@@ -141,18 +138,18 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"))?.includes(control.I);
const Tag = TagType((control.I === 0 || control.I === inkData.length - 1) && !closed) as keyof JSX.IntrinsicElements;
return <Tag key={control.I.toString() + scale}
- x={control.X - screenSpaceLineWidth * 2 * scale}
- y={control.Y - screenSpaceLineWidth * 2 * scale}
+ x={control.X - this.props.screenSpaceLineWidth * 2 * scale}
+ y={control.Y - this.props.screenSpaceLineWidth * 2 * scale}
cx={control.X}
cy={control.Y}
- r={screenSpaceLineWidth * 2 * scale}
+ r={this.props.screenSpaceLineWidth * 2 * scale}
opacity={this.controlUndo ? 0.15 : 1}
- height={screenSpaceLineWidth * 4 * scale}
- width={screenSpaceLineWidth * 4 * scale}
- strokeWidth={screenSpaceLineWidth / 2}
+ height={this.props.screenSpaceLineWidth * 4 * scale}
+ width={this.props.screenSpaceLineWidth * 4 * scale}
+ strokeWidth={this.props.screenSpaceLineWidth / 2}
stroke={Colors.MEDIUM_BLUE}
fill={broken ? Colors.MEDIUM_BLUE : color}
- onPointerDown={(e: any) => this.onControlDown(e, control.I)}
+ onPointerDown={(e: React.PointerEvent) => this.onControlDown(e, control.I)}
onMouseEnter={() => this.onEnterControl(control.I)}
onMouseLeave={this.onLeaveControl}
pointerEvents="all"
@@ -164,7 +161,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
<circle key={"npt"}
cx={nearestScreenPt.X}
cy={nearestScreenPt.Y}
- r={screenSpaceLineWidth * 2}
+ r={this.props.screenSpaceLineWidth * 2}
fill={"#00007777"}
stroke={"#00007777"}
strokeWidth={0}
@@ -175,4 +172,62 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
</svg>
);
}
+}
+
+
+export interface InkEndProps {
+ inkDoc: Doc;
+ inkView: DocumentView;
+ screenSpaceLineWidth: number;
+ startPt: PointData;
+ endPt: PointData;
+}
+@observer
+export class InkEndPtHandles extends React.Component<InkEndProps> {
+ @observable controlUndo: UndoManager.Batch | undefined;
+ @observable _overStart: boolean = false;
+ @observable _overEnd: boolean = false;
+
+ @action
+ dragRotate = (e: React.PointerEvent, p1: () => { X: number, Y: number }, p2: () => { X: number, Y: number }) => {
+ setupMoveUpEvents(this, e, (e) => {
+ if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("stretch ink");
+ // compute stretch factor by finding scaling along axis between start and end points
+ const v1 = { X: p1().X - p2().X, Y: p1().Y - p2().Y };
+ const v2 = { X: e.clientX - p2().X, Y: e.clientY - p2().Y };
+ const v1len = Math.sqrt(v1.X * v1.X + v1.Y * v1.Y);
+ const v2len = Math.sqrt(v2.X * v2.X + v2.Y * v2.Y);
+ const scaling = v2len / v1len;
+ const v1n = { X: v1.X / v1len, Y: v1.Y / v1len };
+ const v2n = { X: v2.X / v2len, Y: v2.Y / v2len };
+ const angle = Math.acos(v1n.X * v2n.X + v1n.Y * v2n.Y) * Math.sign(v1.X * v2.Y - v2.X * v1.Y);
+ InkStrokeProperties.Instance.stretchInk(SelectionManager.Views(), scaling, p2(), v1n, e.shiftKey);
+ InkStrokeProperties.Instance.rotateInk(SelectionManager.Views(), angle, p2());
+ return false;
+ }, action(() => {
+ this.controlUndo?.end();
+ this.controlUndo = undefined;
+ UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
+ }), returnFalse);
+ }
+
+ render() {
+ const hdl = (key: string, pt: PointData, dragFunc: (e: React.PointerEvent) => void) => <circle key={key}
+ cx={pt.X}
+ cy={pt.Y}
+ r={this.props.screenSpaceLineWidth * 2}
+ fill={this._overStart ? "#aaaaaa" : "#99999977"}
+ stroke={"#00007777"}
+ strokeWidth={0}
+ onPointerLeave={action(() => this._overStart = false)}
+ onPointerEnter={action(() => this._overStart = true)}
+ onPointerDown={dragFunc}
+ pointerEvents="all"
+ />;
+ return (<svg>
+ {hdl("start", this.props.startPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.startPt, () => this.props.endPt))}
+ {hdl("end", this.props.endPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.endPt, () => this.props.startPt))}
+ </svg>
+ );
+ }
} \ No newline at end of file
diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss
index 55e06c6ca..664f2448b 100644
--- a/src/client/views/InkStroke.scss
+++ b/src/client/views/InkStroke.scss
@@ -13,16 +13,28 @@
}
}
-.inkStroke {
- mix-blend-mode: multiply;
- stroke-linejoin: round;
- stroke-linecap: round;
- overflow: visible !important;
- transform-origin: top left;
- width: 100%;
- height: 100%;
+.inkStroke-wrapper {
+ display: flex;
+ align-items: center;
+ height: 100%;
+ .inkStroke {
+ mix-blend-mode: multiply;
+ stroke-linejoin: round;
+ stroke-linecap: round;
+ overflow: visible !important;
+ transform-origin: top left;
+ width: 100%;
+ height: 100%;
+ pointer-events: none;
+ svg:not(:root) {
+ overflow: visible !important;
+ }
+ }
- svg:not(:root) {
- overflow: visible !important;
- }
+ .inkStroke-text {
+ position: absolute;
+ &:hover {
+ background: #9f9f9f0a;
+ }
+ }
}
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 6687b2bc7..cab4e1216 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -1,26 +1,30 @@
import { Bezier } from "bezier-js";
+import { Normalize, Distance } from "../util/bezierFit";
import { action, observable, reaction } from "mobx";
-import { Doc, Opt, DocListCast } from "../../fields/Doc";
+import { Doc, NumListCast, Opt } from "../../fields/Doc";
import { InkData, InkField, InkTool, PointData } from "../../fields/InkField";
import { List } from "../../fields/List";
import { listSpec } from "../../fields/Schema";
import { Cast, NumCast } from "../../fields/Types";
+import { Point } from "../../pen-gestures/ndollar";
import { DocumentType } from "../documents/DocumentTypes";
+import { FitOneCurve } from "../util/bezierFit";
import { CurrentUserUtils } from "../util/CurrentUserUtils";
+import { DocumentManager } from "../util/DocumentManager";
import { undoBatch } from "../util/UndoManager";
import { InkingStroke } from "./InkingStroke";
import { DocumentView } from "./nodes/DocumentView";
-import { DocumentManager } from "../util/DocumentManager";
export class InkStrokeProperties {
- static Instance: InkStrokeProperties | undefined;
+ static _Instance: InkStrokeProperties | undefined;
+ public static get Instance() { return this._Instance || new InkStrokeProperties(); }
@observable _lock = false;
@observable _controlButton = false;
@observable _currentPoint = -1;
constructor() {
- InkStrokeProperties.Instance = this;
+ InkStrokeProperties._Instance = this;
reaction(() => this._controlButton, button => button && (CurrentUserUtils.SelectedTool = InkTool.None));
reaction(() => CurrentUserUtils.SelectedTool, tool => (tool !== InkTool.None) && (this._controlButton = false));
}
@@ -139,18 +143,35 @@ export class InkStrokeProperties {
*/
@undoBatch
@action
- deletePoints = (inkView: DocumentView) => this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
+ deletePoints = (inkView: DocumentView, preserve: boolean) => this.applyFunction(inkView, (view: DocumentView, ink: InkData) => {
const doc = view.rootDoc;
- const newPoints: { X: number, Y: number }[] = [];
- const toRemove = Math.floor((this._currentPoint + 2) / 4);
- const last = this._currentPoint === ink.length - 1;
- for (let i = 0; i < ink.length; i++) {
- if (Math.floor((i + 2) / 4) !== toRemove && (toRemove !== 0 || i > 3)) {
- newPoints.push({ X: ink[i].X, Y: ink[i].Y });
+ const newPoints = ink.slice();
+ const brokenIndices = NumListCast(doc.brokenInkIndices);
+ if (preserve || this._currentPoint === 0 || this._currentPoint === ink.length - 1 || brokenIndices.includes(this._currentPoint)) {
+ newPoints.splice(this._currentPoint === 0 ? 0 : this._currentPoint === ink.length - 1 ? this._currentPoint - 3 : this._currentPoint - 2, 4);
+ } else {
+ const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
+ const splicedPoints = ink.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
+ const samples: Point[] = [];
+ var startDir = { x: 0, y: 0 };
+ var endDir = { x: 0, y: 0 };
+ for (var i = 0; i < splicedPoints.length / 4; i++) {
+ const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+ if (i === 0) startDir = bez.derivative(0);
+ if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
+ for (var t = 0; t < (i === splicedPoints.length / 4 - 1 ? 1 + 1e-7 : 1); t += 0.05) {
+ const pt = bez.compute(t);
+ samples.push(new Point(pt.x, pt.y));
+ }
+ }
+ const { finalCtrls, error } = FitOneCurve(samples, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+ if (error < 100) {
+ newPoints.splice(this._currentPoint - 4, 8, ...finalCtrls);
+ } else {
+ newPoints.splice(this._currentPoint - 2, 4);
}
}
- doc.brokenInkIndices = new List(Cast(doc.brokenInkIndices, listSpec("number"), []).map(control => control >= toRemove * 4 ? control - 4 : control));
- if (last) newPoints.splice(newPoints.length - 3, 2);
+ doc.brokenInkIndices = new List(brokenIndices.map(control => control >= this._currentPoint ? control - 4 : control));
this._currentPoint = -1;
return newPoints.length < 4 ? undefined : newPoints;
}, true)
@@ -163,10 +184,10 @@ export class InkStrokeProperties {
*/
@undoBatch
@action
- rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: { x: number, y: number }) => {
+ rotateInk = (inkStrokes: DocumentView[], angle: number, scrpt: PointData) => {
this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => {
view.rootDoc.rotation = NumCast(view.rootDoc.rotation) + angle;
- const inkCenterPt = view.ComponentView?.ptFromScreen?.({ X: scrpt.x, Y: scrpt.y });
+ const inkCenterPt = view.ComponentView?.ptFromScreen?.(scrpt);
return !inkCenterPt ? ink :
ink.map(i => {
const pt = { X: i.X - inkCenterPt.X, Y: i.Y - inkCenterPt.Y };
@@ -178,19 +199,84 @@ export class InkStrokeProperties {
}
/**
+ * Rotates ink stroke(s) about a point
+ * @param inkStrokes set of ink documentViews to rotate
+ * @param angle The angle at which to rotate the ink in radians.
+ * @param scrpt The center point of the rotation in screen coordinates
+ */
+ @undoBatch
+ @action
+ stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => {
+ this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => {
+ const ptFromScreen = view.ComponentView?.ptFromScreen;
+ const ptToScreen = view.ComponentView?.ptToScreen;
+ return !ptToScreen || !ptFromScreen ? ink :
+ ink.map(ptToScreen).map(i => {
+ const pvec = { X: i.X - scrpt.X, Y: i.Y - scrpt.Y };
+ const svec = pvec.X * scrVec.X * scaling + pvec.Y * scrVec.Y * scaling;
+ const ovec = -pvec.X * scrVec.Y * (scaleUniformly ? scaling : 1) + pvec.Y * scrVec.X * (scaleUniformly ? scaling : 1);
+ const newscrpt = { X: scrpt.X + svec * scrVec.X - ovec * scrVec.Y, Y: scrpt.Y + svec * scrVec.Y + ovec * scrVec.X };
+ return ptFromScreen(newscrpt);
+ });
+ });
+ }
+
+ /**
* Handles the movement/scaling of a control point.
*/
@undoBatch
@action
- moveControlPtHandle = (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number) =>
+ moveControlPtHandle = (inkView: DocumentView, deltaX: number, deltaY: number, controlIndex: number, origInk?: InkData) =>
this.applyFunction(inkView, (view: DocumentView, ink: InkData, xScale: number, yScale: number) => {
const order = controlIndex % 4;
const closed = InkingStroke.IsClosed(ink);
+ if (origInk && this._currentPoint > 0 && this._currentPoint < ink.length - 1) {
+ const cpt_before = ink[controlIndex];
+ const cpt = { X: cpt_before.X + deltaX, Y: cpt_before.Y + deltaY };
+ if (true) {
+ const newink = origInk.slice();
+ const start = this._currentPoint === 0 ? 0 : this._currentPoint - 4;
+ const splicedPoints = origInk.slice(start, start + (this._currentPoint === 0 || this._currentPoint === ink.length - 1 ? 4 : 8));
+ const { nearestT, nearestSeg } = InkStrokeProperties.nearestPtToStroke(splicedPoints, cpt);
+ const samplesLeft: Point[] = [];
+ const samplesRight: Point[] = [];
+ var startDir = { x: 0, y: 0 };
+ var endDir = { x: 0, y: 0 };
+ for (var i = 0; i < nearestSeg / 4 + 1; i++) {
+ const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+ if (i === 0) startDir = bez.derivative(0);
+ if (i === nearestSeg / 4) endDir = bez.derivative(nearestT);
+ for (var t = 0; t < (i === nearestSeg / 4 ? nearestT + .05 : 1); t += 0.05) {
+ const pt = bez.compute(i !== nearestSeg / 4 ? t : Math.min(nearestT, t));
+ samplesLeft.push(new Point(pt.x, pt.y));
+ }
+ }
+ var { finalCtrls, error } = FitOneCurve(samplesLeft, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+ for (var i = nearestSeg / 4; i < splicedPoints.length / 4; i++) {
+ const bez = new Bezier(splicedPoints.slice(i * 4, i * 4 + 4).map(p => ({ x: p.X, y: p.Y })));
+ if (i === nearestSeg / 4) startDir = bez.derivative(nearestT);
+ if (i === splicedPoints.length / 4 - 1) endDir = bez.derivative(1);
+ for (var t = i === nearestSeg / 4 ? nearestT : 0; t < (i === nearestSeg / 4 ? 1 + .05 + 1e-7 : 1 + 1e-7); t += 0.05) {
+ const pt = bez.compute(Math.min(1, t));
+ samplesRight.push(new Point(pt.x, pt.y));
+ }
+ }
+ const { finalCtrls: rightCtrls, error: errorRight } = FitOneCurve(samplesRight, { X: startDir.x, Y: startDir.y }, { X: endDir.x, Y: endDir.y });
+ finalCtrls = finalCtrls.concat(rightCtrls);
+ newink.splice(this._currentPoint - 4, 8, ...finalCtrls);
+ return newink;
+ }
+ }
- const newpts = ink.map((pt, i) => {
+ return ink.map((pt, i) => {
const leftHandlePoint = order === 0 && i === controlIndex + 1;
const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2;
if (controlIndex === i ||
+ (order === 0 && controlIndex !== 0 && i === controlIndex - 1) ||
+ (order === 3 && i === controlIndex - 1)) {
+ return ({ X: pt.X + deltaX, Y: pt.Y + deltaY });
+ }
+ if (controlIndex === i ||
leftHandlePoint ||
rightHandlePoint ||
(order === 0 && controlIndex !== 0 && i === controlIndex - 1) ||
@@ -203,7 +289,6 @@ export class InkStrokeProperties {
}
return pt;
});
- return newpts;
})
@@ -243,8 +328,8 @@ export class InkStrokeProperties {
if (snapData.distance < 10) {
const deltaX = (snapData.nearestPt.X - ink[controlIndex].X);
const deltaY = (snapData.nearestPt.Y - ink[controlIndex].Y);
- const res = this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex);
- console.log("X= " + snapData.nearestPt.X + " " + snapData.nearestPt.Y);
+ const res = this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex, ink.slice());
+ console.log("X = " + snapData.nearestPt.X + " " + snapData.nearestPt.Y);
return res;
}
}
@@ -296,7 +381,7 @@ export class InkStrokeProperties {
brokenIndices.splice(ind, 1);
const [controlPoint, handleA, handleB] = [ink[controlIndex], ink[handleIndexA], ink[handleIndexB]];
const oppositeHandleA = this.rotatePoint(handleA, controlPoint, Math.PI);
- const angleDifference = this.angleChange(handleB, oppositeHandleA, controlPoint);
+ const angleDifference = InkStrokeProperties.angleChange(handleB, oppositeHandleA, controlPoint);
const inkCopy = ink.slice(); // have to make a new copy of the array to keep from corrupting undo/redo. without slicing, the same array will be stored in each undo step meaning earlier undo steps will be inadvertently updated to store the latest value.
inkCopy[handleIndexB] = this.rotatePoint(handleB, controlPoint, angleDifference);
return inkCopy;
@@ -320,7 +405,7 @@ export class InkStrokeProperties {
*
* α = arccos(a·b / |a|·|b|), where a and b are both vectors.
*/
- angleBetweenTwoVectors = (vectorA: PointData, vectorB: PointData) => {
+ public static angleBetweenTwoVectors(vectorA: PointData, vectorB: PointData) {
const magnitudeA = Math.sqrt(vectorA.X * vectorA.X + vectorA.Y * vectorA.Y);
const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y);
if (magnitudeA === 0 || magnitudeB === 0) return 0;
@@ -333,14 +418,14 @@ export class InkStrokeProperties {
/**
* Finds the angle difference (in radians) between two vectors relative to an arbitrary origin.
*/
- angleChange = (a: PointData, b: PointData, origin: PointData) => {
+ public static angleChange(a: PointData, b: PointData, origin: PointData) {
// Finding vector representation of inputted points relative to new origin.
const vectorA = { X: a.X - origin.X, Y: a.Y - origin.Y };
const vectorB = { X: b.X - origin.X, Y: b.Y - origin.Y };
const crossProduct = vectorB.X * vectorA.Y - vectorB.Y * vectorA.X;
// Determining whether rotation is clockwise or counterclockwise.
const sign = crossProduct < 0 ? 1 : -1;
- const theta = this.angleBetweenTwoVectors(vectorA, vectorB);
+ const theta = InkStrokeProperties.angleBetweenTwoVectors(vectorA, vectorB);
return sign * theta;
}
@@ -364,7 +449,7 @@ export class InkStrokeProperties {
// Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) &&
(closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) {
- const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
+ const angle = InkStrokeProperties.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
}
return inkCopy;
diff --git a/src/client/views/InkTangentHandles.tsx b/src/client/views/InkTangentHandles.tsx
index f88a20448..ab73e58a4 100644
--- a/src/client/views/InkTangentHandles.tsx
+++ b/src/client/views/InkTangentHandles.tsx
@@ -29,24 +29,23 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
* @param handleNum The index of the currently selected handle point.
*/
onHandleDown = (e: React.PointerEvent, handleIndex: number): void => {
- if (InkStrokeProperties.Instance) {
- var controlUndo: UndoManager.Batch | undefined;
- const screenScale = this.props.ScreenToLocalTransform().Scale;
- const order = handleIndex % 4;
- const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3;
- const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.screenCtrlPoints.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.screenCtrlPoints.length;
- const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.screenCtrlPoints.length;
- setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => {
+ var controlUndo: UndoManager.Batch | undefined;
+ const screenScale = this.props.ScreenToLocalTransform().Scale;
+ const order = handleIndex % 4;
+ const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3;
+ const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.screenCtrlPoints.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.screenCtrlPoints.length;
+ const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.screenCtrlPoints.length;
+ setupMoveUpEvents(this, e,
+ (e: PointerEvent, down: number[], delta: number[]) => {
if (!controlUndo) controlUndo = UndoManager.StartBatch("DocDecs move tangent");
if (e.altKey) this.onBreakTangent(controlIndex);
- InkStrokeProperties.Instance?.moveTangentHandle(this.props.inkView, -delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex);
+ InkStrokeProperties.Instance.moveTangentHandle(this.props.inkView, -delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex);
return false;
}, () => {
controlUndo?.end();
UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
}, emptyFunction
- );
- }
+ );
}
/**
@@ -66,9 +65,6 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
}
render() {
- const formatInstance = InkStrokeProperties.Instance;
- if (!formatInstance) return (null);
-
// Accessing the current ink's data and extracting all handle points and handle lines.
const data = this.props.screenCtrlPoints;
const tangentHandles: HandlePoint[] = [];
@@ -107,7 +103,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
onPointerDown={e => this.onHandleDown(e, pts.I)}
pointerEvents="all"
cursor="default"
- display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />
+ display={(pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint) ? "inherit" : "none"} />
</svg>)}
{tangentLines.map((pts, i) => {
const tangentLine = (x1: number, y1: number, x2: number, y2: number) =>
@@ -119,7 +115,7 @@ export class InkTangentHandles extends React.Component<InkHandlesProps> {
stroke={Colors.MEDIUM_BLUE}
strokeDasharray={"1 1"}
strokeWidth={1}
- display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} />;
+ display={(pts.dot1 === InkStrokeProperties.Instance._currentPoint || pts.dot2 === InkStrokeProperties.Instance._currentPoint) ? "inherit" : "none"} />;
return <svg height="100" width="100" key={`line${i}`}>
{tangentLine(pts.X1, pts.Y1, pts.X2, pts.Y2)}
{tangentLine(pts.X2, pts.Y2, pts.X3, pts.Y3)}
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index ecb46a5b3..5c7fc94bd 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,62 +1,82 @@
+/*
+ InkingStroke - a document that represents an individual vector stroke drawn as a Bezier curve (open or closed) and optionally filled.
+
+ The primary data is:
+ data - an InkField which is an array of PointData (X,Y values). The data is laid out as a sequence of simple bezier segments:
+ point 1, tangent pt 1, tangent pt 2, point 2, point 3, tangent pt 3, ... (Note that segment endpoints are duplicated ie Point2 = Point 3)
+ brokenIndices - an array of indexes into the data field where the incoming and outgoing tangents are not constrained to be equal
+ text - a text field that will be centered within a closed ink stroke
+ isInkMask - a flag that makes the ink stroke render as a mask over its collection where the stroke itself is mixBlendMode multiplied by
+ the underlying collection content, and everything outside the stroke is covered by a semi-opaque dark gray mask.
+
+ The coordinates of the ink data need to be mapped to the screen since ink points are not changed when the DocumentView is translated or scaled.
+ Thus the mapping can roughly be described by:
+ the Top/Left of the ink data (minus 1/2 the ink width) maps to the Top/Left of the DocumentView
+ the Width/Height of the ink data (minus the ink width) is scaled to the PanelWidth/PanelHeight of the documentView
+ NOTE: use ptToScreen() and ptFromScreen() to transform between ink and screen space
+
+ InkStrokes have a specialized 'componentUI' method that is called by MainView to render all of the interactive editing controls in
+ screen space (to avoid scaling artifacts)
+
+ Most of the operations that can be performed on an InkStroke (eg delete a point, rotate, stretch) are implemented in the InkStrokeProperties helper class
+*/
import React = require("react");
import { action, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
-import { Doc } from "../../fields/Doc";
+import { Doc, WidthSym } from "../../fields/Doc";
import { documentSchema } from "../../fields/documentSchemas";
import { InkData, InkField, InkTool } from "../../fields/InkField";
import { makeInterface } from "../../fields/Schema";
import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types";
import { TraceMobx } from "../../fields/util";
-import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../Utils";
+import { OmitKeys, returnFalse, setupMoveUpEvents } from "../../Utils";
import { CognitiveServices } from "../cognitive_services/CognitiveServices";
-import { CurrentUserUtils } from "../util/CurrentUserUtils";
import { InteractionUtils } from "../util/InteractionUtils";
import { SnappingManager } from "../util/SnappingManager";
+import { Transform } from "../util/Transform";
+import { UndoManager } from "../util/UndoManager";
import { ContextMenu } from "./ContextMenu";
import { ViewBoxBaseComponent } from "./DocComponent";
import { Colors } from "./global/globalEnums";
-import { InkControlPtHandles } from "./InkControlPtHandles";
+import { InkControlPtHandles, InkEndPtHandles } from "./InkControlPtHandles";
import "./InkStroke.scss";
import { InkStrokeProperties } from "./InkStrokeProperties";
import { InkTangentHandles } from "./InkTangentHandles";
import { FieldView, FieldViewProps } from "./nodes/FieldView";
+import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox";
import Color = require("color");
-import { Transform } from "../util/Transform";
type InkDocument = makeInterface<[typeof documentSchema]>;
const InkDocument = makeInterface(documentSchema);
@observer
export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) {
+ static readonly MaskDim = 50000; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big)
public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
- static readonly MaskDim = 50000;
public static IsClosed(inkData: InkData) {
return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
}
- @observable private _properties?: InkStrokeProperties;
- _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
- _selDisposer: IReactionDisposer | undefined;
+ private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
+ private _selDisposer?: IReactionDisposer;
- @observable _nearestT: number | undefined;
- @observable _nearestSeg: number | undefined;
- @observable _nearestScrPt: { X: number, Y: number } | undefined;
- @observable _inkSamplePts: { X: number, Y: number }[] | undefined;
-
- constructor(props: FieldViewProps & InkDocument) {
- super(props);
-
- this._properties = InkStrokeProperties.Instance;
- }
+ @observable _nearestSeg?: number; // nearest Bezier segment along the ink stroke to the cursor (used for displaying the Add Point highlight)
+ @observable _nearestT?: number; // nearest t value within the nearest Bezier segment "
+ @observable _nearestScrPt?: { X: number, Y: number }; // nearst screen point on the ink stroke ""
componentDidMount() {
this.props.setContentView?.(this);
this._selDisposer = reaction(() => this.props.isSelected(), // react to stroke being deselected by turning off ink handles
- selected => !selected && this.toggleControlButton());
+ selected => !selected && (InkStrokeProperties.Instance._controlButton = false));
}
componentWillUnmount() {
this._selDisposer?.();
}
+ /**
+ * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space);
+ * DocumentDecorations calls getBounds() on DocumentViews which call getCenter() if defined - in the case of ink it needs to be defined since
+ * the center of the ink stroke changes as the stroke is rotated.
+ */
getCenter = (xf: Transform) => {
const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
const angle = -NumCast(this.layoutDoc.rotation);
@@ -73,11 +93,21 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
return { X: tc[0], Y: tc[1] };
}
+ /**
+ * analyzes the ink stroke and saves the analysis of the stroke to the 'inkAnalysis' field,
+ * and the recognized words to the 'handwriting'
+ */
analyzeStrokes() {
const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]);
}
+ /**
+ * Toggles whether the ink stroke is displayed as an overlay mask or as a regular stroke.
+ * When displayed as a mask, the stroke is rendered with mixBlendMode set to multiply so that the stroke will
+ * appear to illuminate what it covers up. At the same time, all pixels that are not under the stroke will be
+ * dimmed by a semi-opaque overlay mask.
+ */
public static toggleMask = action((inkDoc: Doc) => {
inkDoc.isInkMask = !inkDoc.isInkMask;
inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined;
@@ -86,44 +116,53 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined;
});
/**
- * Handles the movement of the entire ink object when the user clicks and drags.
+ * Drags the a simple bezier segment of the stroke.
+ * Also adds a control point when double clicking on the stroke.
*/
+ @action
onPointerDown = (e: React.PointerEvent) => {
this._handledClick = false;
- if (this.props.isSelected(true)) {
- setupMoveUpEvents(this, e, returnFalse, emptyFunction,
- action((e: PointerEvent, doubleTap: boolean | undefined) => {
- doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick;
- if (doubleTap && this._properties) {
- this._properties._controlButton = true;
- InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1);
- this._handledClick = true; // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView
- } else if (this._properties?._controlButton) {
- this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
+ const inkView = this.props.docViewPath().lastElement();
+ const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
+ const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint(
+ (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2,
+ (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] }));
+ const { nearestSeg } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY });
+ const controlIndex = nearestSeg;
+ const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex;
+ var controlUndo: UndoManager.Batch | undefined;
+ const isEditing = InkStrokeProperties.Instance._controlButton && this.props.isSelected();
+ setupMoveUpEvents(this, e,
+ !isEditing ? returnFalse : action((e: PointerEvent, down: number[], delta: number[]) => {
+ if (!controlUndo) controlUndo = UndoManager.StartBatch("drag ink ctrl pt");
+ const inkMoveEnd = this.ptFromScreen({ X: delta[0], Y: delta[1] });
+ const inkMoveStart = this.ptFromScreen({ X: 0, Y: 0 });
+ InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex);
+ InkStrokeProperties.Instance.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex + 3);
+ return false;
+ }),
+ !isEditing ? returnFalse : action(() => {
+ controlUndo?.end();
+ controlUndo = undefined;
+ UndoManager.FilterBatches(["data", "x", "y", "width", "height"]);
+ }),
+ action((e: PointerEvent, doubleTap: boolean | undefined) => {
+ doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick;
+ if (doubleTap) {
+ InkStrokeProperties.Instance._controlButton = true;
+ InkStrokeProperties.Instance._currentPoint = -1;
+ this._handledClick = true; // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView
+ if (isEditing) {
+ this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
}
- }), this._properties?._controlButton, this._properties?._controlButton
- );
- }
+ }
+ }), isEditing, isEditing, action(() => wasSelected && (InkStrokeProperties.Instance._currentPoint = -1)));
}
/**
- * Ensures the ink controls and handles aren't rendered when the current ink stroke is reselected.
+ * @param scrPt a point in the screen coordinate space
+ * @returns the point in the ink data's coordinate space.
*/
- @action
- toggleControlButton = () => {
- if (!this.props.isSelected() && this._properties) {
- this._properties._controlButton = false;
- }
- }
-
- @action
- checkHighlighter = () => {
- if (CurrentUserUtils.SelectedTool === InkTool.Highlighter) {
- // this._previousColor = ActiveInkColor();
- SetActiveInkColor("rgba(245, 230, 95, 0.75)");
- }
- }
-
ptFromScreen = (scrPt: { X: number, Y: number }) => {
const { inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
const docPt = this.props.ScreenToLocalTransform().transformPoint(scrPt.X, scrPt.Y);
@@ -133,6 +172,11 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
};
return inkPt;
}
+
+ /**
+ * @param inkPt a point in the ink data's coordinate space
+ * @returns the screen point corresponding to the ink point
+ */
ptToScreen = (inkPt: { X: number, Y: number }) => {
const { inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
const docPt = {
@@ -143,13 +187,23 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
return { X: scrPt[0], Y: scrPt[1] };
}
+ /**
+ * Snaps a screen space point to this stroke, optionally skipping bezier segments indicated by 'excludeSegs'
+ * @param scrPt - the point to snap to this stroke
+ * @param excludeSegs - optional segments in this stroke to skip (this is used when dragging a point on the stroke and not wanting the drag point to snap to its neighboring segments)
+ *
+ * @returns the nearest ink space point on this stroke to the screen point AND the screen space distance from the snapped point to the nearest point
+ */
snapPt = (scrPt: { X: number, Y: number }, excludeSegs?: number[]) => {
const { inkData } = this.inkScaledData();
- const inkPt = this.ptFromScreen(scrPt);
- const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, inkPt, excludeSegs ?? []);
+ const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, this.ptFromScreen(scrPt), excludeSegs ?? []);
return { nearestPt, distance: distance * this.props.ScreenToLocalTransform().inverse().Scale };
}
+ /**
+ * extracts key features from the inkData, including: the data points, the ink width, the ink bounds (top,left, width, height), and the scale
+ * factor for converting between ink and screen space.
+ */
inkScaledData = () => {
const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
const inkStrokeWidth = NumCast(this.rootDoc.strokeWidth, 1);
@@ -184,8 +238,16 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
this._nearestScrPt = nearestPt;
}
-
+ /**
+ * @returns the nearest screen point to the cursor (to render a highlight for the point to be added)
+ */
nearestScreenPt = () => this._nearestScrPt;
+
+ /**
+ * @param boundsLeft the screen space left coordinate of the ink stroke
+ * @param boundsTop the screen space top coordinate of the ink stroke
+ * @returns the JSX controls for displaying an editing UI for the stroke (control point & tangent handles)
+ */
componentUI = (boundsLeft: number, boundsTop: number) => {
const inkDoc = this.props.Document;
const screenSpaceCenterlineStrokeWidth = 3; // the width of the blue line widget that shows the centerline of the ink stroke
@@ -199,14 +261,21 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
- return SnappingManager.GetIsDragging() ? (null) : <div className="inkstroke-UI" style={{
- clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)`
- }} >
- {!this._properties?._controlButton ? (null) :
- <>
+ const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1);
+ return SnappingManager.GetIsDragging() ? (null) :
+ !InkStrokeProperties.Instance._controlButton ?
+ (!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? (null) :
+ <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
+ <InkEndPtHandles
+ inkView={this.props.docViewPath().lastElement()}
+ inkDoc={inkDoc}
+ startPt={this.ptToScreen(inkData[0])}
+ endPt={this.ptToScreen(inkData.lastElement())}
+ screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} /></div>) :
+ <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
{InteractionUtils.CreatePolyline(screenPts, 0, 0, Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth,
StrCast(inkDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(inkDoc.strokeBezier),
- "none", startMarker, endMarker, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)}
+ "none", startMarker, endMarker, markerScale, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)}
<InkControlPtHandles
inkView={this.props.docViewPath().lastElement()}
inkDoc={inkDoc}
@@ -221,8 +290,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
screenCtrlPoints={screenHdlPts}
screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth}
ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
- </>}
- </div>;
+ </div>;
}
render() {
@@ -231,6 +299,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
+ const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1);
const closed = InkingStroke.IsClosed(inkData);
const fillColor = StrCast(this.layoutDoc.fillColor, "transparent");
const strokeColor = !closed && fillColor && fillColor !== "transparent" ? fillColor : StrCast(this.layoutDoc.color);
@@ -239,43 +308,59 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
const inkLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, strokeColor, inkStrokeWidth, inkStrokeWidth,
StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap),
StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker,
- StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, false);
+ markerScale, StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, false);
const highlightIndex = BoolCast(this.props.Document.isLinkButton) && Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString
const highlightColor = !highlightIndex ?
StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== "transparent" ? StrCast(this.layoutDoc.color, "transparent") : "transparent") :
["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex];
// Invisible polygonal line that enables the ink to be selected by the user.
- const clickableLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor,
- inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
+ const clickableLine = (downHdlr?: (e: React.PointerEvent) => void) => InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor,
+ inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && fillColor && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap),
StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker,
- undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0, false);
- // Set of points rendered upon the ink that can be added if a user clicks on one.
+ markerScale, undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0,
+ false, downHdlr);
- return (
+ return <div className="inkStroke-wrapper">
<svg className="inkStroke"
style={{
- pointerEvents: "none",
transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
- overflow: "visible",
cursor: this.props.isSelected() ? "default" : undefined
}}
onPointerLeave={action(e => this._nearestScrPt = undefined)}
onPointerMove={this.props.isSelected() ? this.onPointerMove : undefined}
- onPointerDown={this.onPointerDown}
onClick={e => this._handledClick && e.stopPropagation()}
onContextMenu={() => {
const cm = ContextMenu.Instance;
!Doc.UserDoc().noviceMode && cm?.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" });
cm?.addItem({ description: "Toggle Mask", event: () => InkingStroke.toggleMask(this.rootDoc), icon: "paint-brush" });
- cm?.addItem({ description: "Edit Points", event: action(() => { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" });
+ cm?.addItem({ description: "Edit Points", event: action(() => InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton), icon: "paint-brush" });
}}
>
- {clickableLine}
+ {clickableLine(this.onPointerDown)}
{inkLine}
</svg>
- );
+ {!closed ? (null) :
+ <div className="inkStroke-text" style={{
+ color: StrCast(this.layoutDoc.textColor, "black"),
+ pointerEvents: this.props.isDocumentActive?.() ? "all" : undefined,
+ width: this.layoutDoc[WidthSym](),
+ }}>
+ <FormattedTextBox
+ {...OmitKeys(this.props, ['children']).omit}
+ yPadding={10}
+ xPadding={10}
+ fieldKey={"text"}
+ fontSize={12}
+ dontRegisterView={true}
+ noSidebar={true}
+ dontScale={true}
+ isContentActive={this.isContentActive}
+ />
+ </div>
+ }
+ </div>;
}
}
@@ -286,12 +371,14 @@ export function SetActiveInkColor(value: string) { ActiveInkPen() && (ActiveInkP
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 ActiveInkPen(): Doc { return Doc.UserDoc(); }
export function ActiveInkColor(): string { return StrCast(ActiveInkPen()?.activeInkColor, "black"); }
export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activeFillColor, ""); }
export function ActiveArrowStart(): string { return StrCast(ActiveInkPen()?.activeArrowStart, ""); }
export function ActiveArrowEnd(): string { return StrCast(ActiveInkPen()?.activeArrowEnd, ""); }
+export function ActiveArrowScale(): number { return NumCast(ActiveInkPen()?.activeArrowScale, 1); }
export function ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, "0"); }
export function ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); }
export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); }
diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx
index 2929630b6..0bd6c9166 100644
--- a/src/client/views/MainView.tsx
+++ b/src/client/views/MainView.tsx
@@ -116,7 +116,6 @@ export class MainView extends React.Component {
}, 0);
setTimeout(() => ele.outerHTML = '', 1000);
}
- new InkStrokeProperties();
this._sidebarContent.proto = undefined;
if (!MainView.Live) {
DocServer.setPlaygroundFields(["dataTransition", "treeViewOpen", "autoHeight", "showSidebar", "sidebarWidthPercent", "viewTransition",
@@ -511,7 +510,7 @@ export class MainView extends React.Component {
bringToFront={emptyFunction}
select={emptyFunction}
isAnyChildContentActive={returnFalse}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
isSelected={returnFalse}
docViewPath={returnEmptyDoclist}
moveDocument={this.moveButtonDoc}
@@ -592,7 +591,7 @@ export class MainView extends React.Component {
pinToPres={returnFalse}
ScreenToLocalTransform={Transform.Identity}
bringToFront={returnFalse}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
whenChildContentsActiveChanged={returnFalse}
focus={returnFalse}
docViewPath={returnEmptyDoclist}
@@ -669,7 +668,7 @@ export class MainView extends React.Component {
pinToPres={returnFalse}
ScreenToLocalTransform={Transform.Identity}
bringToFront={returnFalse}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
whenChildContentsActiveChanged={returnFalse}
focus={returnFalse}
PanelWidth={() => 500}
diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx
index af04b967a..7cf388872 100644
--- a/src/client/views/OverlayView.tsx
+++ b/src/client/views/OverlayView.tsx
@@ -191,7 +191,7 @@ export class OverlayView extends React.Component {
ScreenToLocalTransform={Transform.Identity}
renderDepth={1}
isDocumentActive={returnTrue}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
whenChildContentsActiveChanged={emptyFunction}
focus={DocUtils.DefaultFocus}
styleProvider={DefaultStyleProvider}
diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx
index 86ab881bb..529697f71 100644
--- a/src/client/views/Palette.tsx
+++ b/src/client/views/Palette.tsx
@@ -50,7 +50,7 @@ export default class Palette extends React.Component<PaletteProps> {
PanelHeight={() => window.screen.height}
renderDepth={0}
isDocumentActive={returnTrue}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
focus={emptyFunction}
docViewPath={returnEmptyDoclist}
styleProvider={returnEmptyString}
diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx
index ab9022a84..8e2426006 100644
--- a/src/client/views/PropertiesView.tsx
+++ b/src/client/views/PropertiesView.tsx
@@ -295,7 +295,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
freezeDimensions={true}
dontCenter={"y"}
isDocumentActive={returnFalse}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
NativeWidth={layoutDoc.type === DocumentType.RTF ? this.rtfWidth : undefined}
NativeHeight={layoutDoc.type === DocumentType.RTF ? this.rtfHeight : undefined}
PanelWidth={panelWidth}
@@ -535,16 +535,17 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@computed
get controlPointsButton() {
- const formatInstance = InkStrokeProperties.Instance;
- return !formatInstance ? (null) : <div className="inking-button">
+ return <div className="inking-button">
<Tooltip title={<div className="dash-tooltip">{"Edit points"}</div>}>
- <div className="inking-button-points" onPointerDown={action(() => formatInstance._controlButton = !formatInstance._controlButton)} style={{ backgroundColor: formatInstance._controlButton ? "black" : "" }}>
+ <div className="inking-button-points"
+ style={{ backgroundColor: InkStrokeProperties.Instance._controlButton ? "black" : "" }}
+ onPointerDown={action(() => InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton)} >
<FontAwesomeIcon icon="bezier-curve" color="white" size="lg" />
</div>
</Tooltip>
- <Tooltip title={<div className="dash-tooltip">{formatInstance._lock ? "Unlock ratio" : "Lock ratio"}</div>}>
- <div className="inking-button-lock" onPointerDown={action(() => formatInstance._lock = !formatInstance._lock)} >
- <FontAwesomeIcon icon={formatInstance._lock ? "lock" : "unlock"} color="white" size="lg" />
+ <Tooltip title={<div className="dash-tooltip">{InkStrokeProperties.Instance._lock ? "Unlock ratio" : "Lock ratio"}</div>}>
+ <div className="inking-button-lock" onPointerDown={action(() => InkStrokeProperties.Instance._lock = !InkStrokeProperties.Instance._lock)} >
+ <FontAwesomeIcon icon={InkStrokeProperties.Instance._lock ? "lock" : "unlock"} color="white" size="lg" />
</div>
</Tooltip>
<Tooltip title={<div className="dash-tooltip">{"Rotate 90Ëš"}</div>}>
@@ -603,7 +604,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
const oldX = NumCast(this.selectedDoc?.x);
const oldY = NumCast(this.selectedDoc?.y);
this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === "up" ? 10 : - 10));
- InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height)));
+ InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) / oldWidth * NumCast(this.selectedDoc?._height)));
const doc = this.selectedDoc;
if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) {
const ink = Cast(doc.data, InkField)?.inkData;
@@ -625,7 +626,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
const oX = NumCast(this.selectedDoc?.x);
const oY = NumCast(this.selectedDoc?.y);
this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === "up" ? 10 : - 10));
- InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width)));
+ InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) / oHeight * NumCast(this.selectedDoc?._width)));
const docu = this.selectedDoc;
if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) {
const ink = Cast(docu.data, InkField)?.inkData;
@@ -663,12 +664,12 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
set shapeWid(value) {
const oldWidth = NumCast(this.selectedDoc?._width);
this.selectedDoc && (this.selectedDoc._width = Number(value));
- InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth);
+ InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._height = (NumCast(this.selectedDoc?._width) * NumCast(this.selectedDoc?._height)) / oldWidth);
}
set shapeHgt(value) {
const oldHeight = NumCast(this.selectedDoc?._height);
this.selectedDoc && (this.selectedDoc._height = Number(value));
- InkStrokeProperties.Instance?._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight);
+ InkStrokeProperties.Instance._lock && this.selectedDoc && (this.selectedDoc._width = (NumCast(this.selectedDoc?._height) * NumCast(this.selectedDoc?._width)) / oldHeight);
}
@computed get hgtInput() { return this.inputBoxDuo("hgt", this.shapeHgt, (val: string) => { if (!isNaN(Number(val))) { this.shapeHgt = val; } return true; }, "H:", "wid", this.shapeWid, (val: string) => { if (!isNaN(Number(val))) { this.shapeWid = val; } return true; }, "W:"); }
@@ -755,6 +756,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@computed get dashdStk() { return this.selectedDoc?.strokeDash || ""; }
@computed get unStrokd() { return this.selectedDoc?.color ? true : false; }
@computed get widthStk() { return this.getField("strokeWidth") || "1"; }
+ @computed get markScal() { return Number(this.getField("strokeMakerScale") || "1"); }
@computed get markHead() { return this.getField("strokeStartMarker") || ""; }
@computed get markTail() { return this.getField("strokeEndMarker") || ""; }
set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; }
@@ -762,6 +764,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
value && (this._lastDash = value) && (this.unStrokd = false);
this.selectedDoc && (this.selectedDoc.strokeDash = value ? this._lastDash : undefined);
}
+ set markScal(value) { this.selectedDoc && (this.selectedDoc.strokeMarkerScale = Number(value)); }
set widthStk(value) { this.selectedDoc && (this.selectedDoc.strokeWidth = Number(value)); }
set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; }
set markHead(value) { this.selectedDoc && (this.selectedDoc.strokeStartMarker = value); }
@@ -769,6 +772,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
@computed get stkInput() { return this.regInput("stk", this.widthStk, (val: string) => this.widthStk = val); }
+ @computed get markScaleInput() { return this.regInput("scale", this.markScal.toString(), (val: string) => this.markScal = Number(val)); }
regInput = (key: string, value: any, setter: (val: string) => {}) => {
@@ -806,6 +810,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> {
<div className="arrows">
<div className="arrows-head">
+ <div className="width-top">
+ <div className="width-title">Arrow Scale:</div>
+ {/* <div className="width-input">{this.markScalInput}</div> */}
+ </div>
+ <input className="width-range" type="range"
+ defaultValue={this.markScal} min={0} max={10}
+ onChange={(action(e => this.markScal = +e.target.value))}
+ onMouseDown={(e) => { this._widthUndo = UndoManager.StartBatch("scale undo"); }}
+ onMouseUp={(e) => { this._widthUndo?.end(); this._widthUndo = undefined; }}
+ />
+ </div>
+ <div className="arrows-head">
<div className="arrows-head-title" >Arrow Head: </div>
<input key="markHead" className="arrows-head-input" type="checkbox"
checked={this.markHead !== ""}
diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx
index ed841d0f5..8ee673115 100644
--- a/src/client/views/StyleProvider.tsx
+++ b/src/client/views/StyleProvider.tsx
@@ -46,6 +46,7 @@ export enum StyleProp {
JitterRotation = "jitterRotation", // whether documents should be randomly rotated
BorderPath = "customBorder", // border path for document view
FontSize = "fontSize", // size of text font
+ FontFamily = "fontFamily", // size of text font
}
function darkScheme() { return CurrentUserUtils.ActiveDashboard?.colorScheme === ColorScheme.Dark; }
@@ -91,7 +92,8 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps
case StyleProp.WidgetColor: return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? "lightgrey" : "dimgrey";
case StyleProp.Opacity: return Cast(doc?._opacity, "number", Cast(doc?.opacity, "number", null));
case StyleProp.HideLinkButton: return props?.hideLinkButton || (!selected && (doc?.isLinkButton || doc?.hideLinkButton));
- case StyleProp.FontSize: return StrCast(doc?.[fieldKey + "fontSize"]);
+ case StyleProp.FontSize: return StrCast(doc?.[fieldKey + "fontSize"], StrCast(doc?.fontSize, StrCast(Doc.UserDoc().fontSize)));
+ case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + "fontFamily"], StrCast(doc?.fontFamily, StrCast(Doc.UserDoc().fontFamily)));
case StyleProp.ShowTitle: return (doc && !doc.presentationTargetDoc &&
StrCast(doc._showTitle,
props?.showTitle?.() ||
diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx
index 648ff5087..bffaf86b1 100644
--- a/src/client/views/collections/CollectionStackingView.tsx
+++ b/src/client/views/collections/CollectionStackingView.tsx
@@ -226,7 +226,7 @@ export class CollectionStackingView extends CollectionSubView<StackingDocument,
layerProvider={this.props.layerProvider}
docViewPath={this.props.docViewPath}
fitWidth={this.props.childFitWidth}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
isDocumentActive={this.isContentActive}
LayoutTemplate={this.props.childLayoutTemplate}
LayoutTemplateString={this.props.childLayoutString}
diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx
index 5dffc65fc..fc1bcb8b9 100644
--- a/src/client/views/collections/CollectionSubView.tsx
+++ b/src/client/views/collections/CollectionSubView.tsx
@@ -22,7 +22,6 @@ import ReactLoading from 'react-loading';
export interface SubCollectionViewProps extends CollectionViewProps {
CollectionView: Opt<CollectionView>;
- SetSubView?: (subView: any) => void;
isAnyChildContentActive: () => boolean;
}
@@ -49,10 +48,6 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
this.createDashEventsTarget(ele);
}
- componentDidMount() {
- this.props.SetSubView?.(this);
- }
-
componentWillUnmount() {
this.gestureDisposer?.();
this._multiTouchDisposer?.();
@@ -220,7 +215,7 @@ export function CollectionSubView<T, X>(schemaCtor: (doc: Doc) => T, moreProps?:
const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d);
const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d);
if (movedDocs.length) {
- const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || !this.props.isAnnotationOverlay ||
+ const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || (!this.props.isAnnotationOverlay || this.props.Document.allowOverlayDrop) ||
Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document);
added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse);
} else {
diff --git a/src/client/views/collections/CollectionTreeView.scss b/src/client/views/collections/CollectionTreeView.scss
index d370d21ab..b664d9d82 100644
--- a/src/client/views/collections/CollectionTreeView.scss
+++ b/src/client/views/collections/CollectionTreeView.scss
@@ -71,6 +71,15 @@
display: none;
}
+.collectionTreeView-titleBar {
+ display: inline-block;
+ width: 100%;
+ height: max-content;
+ .contentFittingDocumentView {
+ display: block; // makes titleBar take up full width of the treeView (flex doesn't for some reason)
+ }
+}
+
.collectionTreeView-keyHeader:hover {
background: #797777;
cursor: pointer;
diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx
index 3852987b9..ea077ea40 100644
--- a/src/client/views/collections/CollectionTreeView.tsx
+++ b/src/client/views/collections/CollectionTreeView.tsx
@@ -1,4 +1,3 @@
-import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { action, computed, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc';
@@ -8,13 +7,14 @@ import { Document, listSpec } from '../../../fields/Schema';
import { ScriptField } from '../../../fields/ScriptField';
import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../fields/Types';
import { TraceMobx } from '../../../fields/util';
-import { returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, emptyFunction } from '../../../Utils';
+import { emptyFunction, OmitKeys, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnOne } from '../../../Utils';
import { DocUtils } from '../../documents/Documents';
import { CurrentUserUtils } from '../../util/CurrentUserUtils';
import { DocumentManager } from '../../util/DocumentManager';
import { DragManager, dropActionType } from "../../util/DragManager";
import { SelectionManager } from '../../util/SelectionManager';
import { SnappingManager } from '../../util/SnappingManager';
+import { Transform } from '../../util/Transform';
import { undoBatch, UndoManager } from '../../util/UndoManager';
import { ContextMenu } from '../ContextMenu';
import { ContextMenuProps } from '../ContextMenuItem';
@@ -22,11 +22,11 @@ import { EditableView } from "../EditableView";
import { DocumentView } from '../nodes/DocumentView';
import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox';
import { StyleProp } from '../StyleProvider';
+import { CollectionFreeFormView } from './collectionFreeForm';
import { CollectionSubView } from "./CollectionSubView";
import "./CollectionTreeView.scss";
import { TreeView } from "./TreeView";
import React = require("react");
-import { Transform } from '../../util/Transform';
const _global = (window /* browser */ || global /* node */) as any;
export type collectionTreeViewProps = {
@@ -41,10 +41,14 @@ export type collectionTreeViewProps = {
@observer
export class CollectionTreeView extends CollectionSubView<Document, Partial<collectionTreeViewProps>>(Document) {
- private treedropDisposer?: DragManager.DragDropDisposer;
+ private _treedropDisposer?: DragManager.DragDropDisposer;
private _mainEle?: HTMLDivElement;
+ private _titleRef?: HTMLDivElement | HTMLInputElement | null;
private _disposers: { [name: string]: IReactionDisposer } = {};
- MainEle = () => this._mainEle;
+ private _isDisposing = false; // notes that instance is in process of being disposed
+ private refList: Set<any> = new Set(); // list of tree view items to monitor for height changes
+ private observer: any; // observer for monitoring tree view items.
+ private static expandViewLabelSize = 20;
@computed get doc() { return this.props.Document; }
@computed get dataDoc() { return this.props.DataDoc || this.doc; }
@@ -54,6 +58,10 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
@computed get fileSysMode() { return this.doc.treeViewType === "fileSystem"; }
@computed get dashboardMode() { return this.doc === Doc.UserDoc().myDashboards; }
+ @observable _explainerHeight = 0; // height of the description of the tree view
+
+ MainEle = () => this._mainEle;
+
// these should stay in synch with counterparts in DocComponent.ts ViewBoxAnnotatableComponent
@observable _isAnyChildContentActive = false;
whenChildContentsActiveChanged = action((isActive: boolean) => this.props.whenChildContentsActiveChanged(this._isAnyChildContentActive = isActive));
@@ -62,11 +70,10 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
this.props.isSelected(outsideReaction) || this._isAnyChildContentActive ||
this.props.rootSelected(outsideReaction)) ? true : false)
- isDisposing = false;
componentWillUnmount() {
- this.isDisposing = true;
+ this._isDisposing = true;
super.componentWillUnmount();
- this.treedropDisposer?.();
+ this._treedropDisposer?.();
Object.values(this._disposers).forEach(disposer => disposer?.());
}
@@ -76,13 +83,13 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
{ fireImmediately: true });
}
- refList: Set<any> = new Set();
- observer: any;
computeHeight = () => {
- if (this.isDisposing) return;
- const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.paddingTop() + this.paddingBot());
- this.layoutDoc._autoHeightMargins = bodyHeight;
- this.props.setHeight(this.documentTitleHeight() + bodyHeight);
+ if (!this._isDisposing) {
+ const titleHeight = !this._titleRef ? this.marginTop() : Number(getComputedStyle(this._titleRef).height.replace("px", ""));
+ const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace("px", "")), this.marginBot());
+ this.layoutDoc._autoHeightMargins = bodyHeight;
+ this.props.setHeight(bodyHeight + titleHeight);
+ }
}
unobserveHeight = (ref: any) => {
this.refList.delete(ref);
@@ -101,8 +108,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
}
}
protected createTreeDropTarget = (ele: HTMLDivElement) => {
- this.treedropDisposer?.();
- if (this._mainEle = ele) this.treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this));
+ this._treedropDisposer?.();
+ if (this._mainEle = ele) this._treedropDisposer = DragManager.MakeDropTarget(ele, this.onInternalDrop.bind(this), this.doc, this.onInternalPreDrop.bind(this));
}
protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
@@ -165,60 +172,44 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
this.addDoc(TreeView.makeTextBullet(), childDocs.length ? childDocs[0] : undefined, true);
}
- editableTitle = (childDocs: Doc[]) => {
- return !this.dataDoc ? (null) :
- <EditableView
- contents={this.dataDoc.title}
- display={"block"}
- maxHeight={72}
- height={"auto"}
- GetValue={() => StrCast(this.dataDoc.title)}
- SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => {
- if (enter && this.props.Document.treeViewType === "outline") this.makeTextCollection(childDocs);
- this.dataDoc.title = value;
- return true;
- })} />;
+ get editableTitle() {
+ return <EditableView
+ contents={this.dataDoc.title}
+ display={"block"}
+ maxHeight={72}
+ height={"auto"}
+ GetValue={() => StrCast(this.dataDoc.title)}
+ SetValue={undoBatch((value: string, shift: boolean, enter: boolean) => {
+ if (enter && this.props.Document.treeViewType === "outline") this.makeTextCollection(this.treeChildren);
+ this.dataDoc.title = value;
+ return true;
+ })} />;
}
- documentTitle = (childDocs: Doc[]) => {
- return <div style={{ display: "inline-block", width: "100%", height: this.documentTitleHeight() }} key={this.doc[Id]}
- onKeyDown={e => {
- e.stopPropagation();
- e.key === "Enter" && this.makeTextCollection(childDocs);
- }}>
- <DocumentView
- Document={this.doc}
- DataDoc={undefined}
- LayoutTemplateString={FormattedTextBox.LayoutString("text")}
- renderDepth={this.props.renderDepth + 1}
- isContentActive={this.isContentActive}
- isDocumentActive={this.isContentActive}
- rootSelected={returnTrue}
- docViewPath={this.props.docViewPath}
- styleProvider={this.props.styleProvider}
- layerProvider={this.props.layerProvider}
- PanelWidth={this.documentTitleWidth}
- PanelHeight={this.documentTitleHeight}
- NativeWidth={this.documentTitleWidth}
- NativeHeight={this.documentTitleHeight}
- focus={this.props.focus}
- treeViewDoc={this.props.Document}
- ScreenToLocalTransform={this.titleTransform}
- docFilters={returnEmptyFilter}
- docRangeFilters={returnEmptyFilter}
- searchFilterDocs={returnEmptyDoclist}
- ContainingCollectionDoc={this.doc}
- ContainingCollectionView={this.props.CollectionView}
- addDocument={this.props.addDocument}
- moveDocument={returnFalse}
- removeDocument={returnFalse}
- whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
- addDocTab={this.props.addDocTab}
- pinToPres={this.props.pinToPres}
- bringToFront={returnFalse}
- />
- </div>;
+ get documentTitle() {
+ return <FormattedTextBox
+ {...this.props}
+ fieldKey={"text"}
+ renderDepth={this.props.renderDepth + 1}
+ isContentActive={this.isContentActive}
+ isDocumentActive={this.isContentActive}
+ rootSelected={returnTrue}
+ forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not autoHeight
+ PanelWidth={this.documentTitleWidth}
+ PanelHeight={this.documentTitleHeight}
+ scaling={returnOne}
+ docFilters={returnEmptyFilter}
+ docRangeFilters={returnEmptyFilter}
+ searchFilterDocs={returnEmptyDoclist}
+ ContainingCollectionDoc={this.doc}
+ ContainingCollectionView={this.props.CollectionView}
+ addDocument={returnFalse}
+ moveDocument={returnFalse}
+ removeDocument={returnFalse}
+ whenChildContentsActiveChanged={this.whenChildContentsActiveChanged}
+ bringToFront={returnFalse}
+ />;
}
childContextMenuItems = () => {
const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
@@ -263,21 +254,31 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
);
}
@computed get titleBar() {
- const hideTitle = this.props.treeViewHideTitle || this.doc.treeViewHideTitle;
- return hideTitle ? (null) : (this.outlineMode ? this.documentTitle : this.editableTitle)(this.treeChildren);
+ return this.dataDoc === null ? (null) :
+ <div className="collectionTreeView-titleBar" key={this.doc[Id]}
+ style={!this.outlineMode ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}}
+ ref={r => this._titleRef = r}
+ onKeyDown={e => {
+ if (this.outlineMode) {
+ e.stopPropagation();
+ e.key === "Enter" && this.makeTextCollection(this.treeChildren);
+ }
+ }}>
+ {this.outlineMode ? this.documentTitle : this.editableTitle}
+ </div>;
+ }
+
+ @computed get noviceExplainer() {
+ return !Doc.UserDoc().noviceMode || !this.rootDoc.explainer ? (null) :
+ <div className="documentExplanation"> {this.rootDoc.explainer} </div>;
}
return35 = () => 35;
@computed get buttonMenu() {
- const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
+ const menuDoc = Cast(this.rootDoc.buttonMenuDoc, Doc, null);
// To create a multibutton menu add a CollectionLinearView
- if (menuDoc) {
-
- const width: number = NumCast(menuDoc._width, 30);
- const height: number = NumCast(menuDoc._height, 30);
- console.log(menuDoc.title, width, height);
- return (<div className="buttonMenu-docBtn"
- style={{ width: width, height: height }}>
+ return !menuDoc ? null :
+ (<div className="buttonMenu-docBtn" style={{ width: NumCast(menuDoc._width, 30), height: NumCast(menuDoc._height, 30) }}>
<DocumentView
Document={menuDoc}
DataDoc={menuDoc}
@@ -306,11 +307,8 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
ContainingCollectionDoc={undefined}
/>
</div>);
- }
}
- @observable _explainerHeight: number = 0;
-
@computed get nativeWidth() { return Doc.NativeWidth(this.Document, undefined, true); }
@computed get nativeHeight() { return Doc.NativeHeight(this.Document, undefined, true); }
@@ -321,47 +319,81 @@ export class CollectionTreeView extends CollectionSubView<Document, Partial<coll
const wscale = nw ? this.props.PanelWidth() / nw : 1;
return wscale < hscale ? wscale : hscale;
}
- paddingX = () => NumCast(this.doc._xPadding, 15);
- paddingTop = () => NumCast(this.doc._yPadding, 20);
- paddingBot = () => NumCast(this.doc._yPadding, 20);
+ marginX = () => NumCast(this.doc._xMargin);
+ marginTop = () => NumCast(this.doc._yMargin);
+ marginBot = () => NumCast(this.doc._yMargin);
documentTitleWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.panelWidth());
documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins);
- titleTransform = () => this.props.ScreenToLocalTransform().translate(-NumCast(this.doc._xPadding, 10), -NumCast(this.doc._yPadding, 20));
truncateTitleWidth = () => this.treeViewtruncateTitleWidth;
onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick);
- panelWidth = () => (this.props.PanelWidth() - 2 * this.paddingX()) * (this.props.scaling?.() || 1);
- render() {
- TraceMobx();
+ panelWidth = () => Math.max(0, this.props.PanelWidth() - this.marginX() - CollectionTreeView.expandViewLabelSize) * (this.props.scaling?.() || 1);
+
+ addAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.addDocument(doc, `${this.props.fieldKey}-annotations`) || false;
+ remAnnotationDocument = (doc: Doc | Doc[]) => this.props.CollectionView?.removeDocument(doc, `${this.props.fieldKey}-annotations`) || false;
+ moveAnnotationDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) =>
+ this.props.CollectionView?.moveDocument(doc, targetCollection, addDocument, `${this.props.fieldKey}-annotations`) || false
+
+ contentFunc = () => {
const background = () => this.props.styleProvider?.(this.doc, this.props, StyleProp.BackgroundColor);
const pointerEvents = () => !this.props.isContentActive() && !SnappingManager.GetIsDragging() ? "none" : undefined;
- const buttonMenu = this.rootDoc.buttonMenu;
- const noviceExplainer = this.rootDoc.explainer;
-
- return !(this.doc instanceof Doc) || !this.treeChildren ? (null) :
- <>
- {this.titleBar}
+ const titleBar = this.props.treeViewHideTitle || this.doc.treeViewHideTitle ? (null) : this.titleBar;
+ return [
+ <div className="collectionTreeView-contents" key="tree" style={{
+ ...(!titleBar ? { paddingLeft: this.marginX(), paddingTop: this.marginTop() } : {}),
+ overflow: "auto",
+ height: this.layoutDoc._autoHeight ? "max-content" : "100%"
+ }} >
+ {titleBar}
<div className="collectionTreeView-container"
- style={this.outlineMode ? { transform: `scale(${this.contentScaling})`, width: `calc(${100 / this.contentScaling}%)` } : {}}
+ style={{
+ transform: this.outlineMode ? `scale(${this.contentScaling})` : "",
+ paddingLeft: `${this.marginX()}px`,
+ height: "max-content",
+ width: this.outlineMode ? `calc(${100 / this.contentScaling}%)` : ""
+ }}
onContextMenu={this.onContextMenu}>
- {buttonMenu || noviceExplainer ? <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}>
- {buttonMenu ? this.buttonMenu : null}
- {Doc.UserDoc().noviceMode && noviceExplainer ?
- <div className="documentExplanation">
- {noviceExplainer}
- </div>
- : null
- }
- </div> : null}
+ {!this.buttonMenu && !this.noviceExplainer ? (null) :
+ <div className="documentButtonMenu" ref={action((r: HTMLDivElement) => r && (this._explainerHeight = r.getBoundingClientRect().height))}>
+ {this.buttonMenu}
+ {this.noviceExplainer}
+ </div>
+ }
<div className="collectionTreeView-dropTarget"
- style={{ background: background(), height: `calc(100% - ${this._explainerHeight}px)`, paddingLeft: `${this.paddingX()}px`, paddingRight: `${this.paddingX()}px`, paddingBottom: `${this.paddingBot()}px`, paddingTop: `${this.paddingTop()}px`, pointerEvents: pointerEvents() }}
+ style={{
+ background: background(),
+ height: `calc(100% - ${this._explainerHeight}px)`,
+ pointerEvents: pointerEvents()
+ }}
onWheel={e => e.stopPropagation()}
onDrop={this.onTreeDrop}
- ref={this.createTreeDropTarget}>
+ ref={r => !this.doc.treeViewHasOverlay && r && this.createTreeDropTarget(r)}>
<ul className={`no-indent${this.outlineMode ? "-outline" : ""}`} >
{this.treeViewElements}
</ul>
</div >
</div>
- </>;
+ </div>
+ ];
+ }
+ render() {
+ TraceMobx();
+
+ return !(this.doc instanceof Doc) || !this.treeChildren ? (null) :
+ this.doc.treeViewHasOverlay ?
+ <CollectionFreeFormView {...OmitKeys(this.props, ["NativeWidth", "NativeHeight", "setContentView"]).omit}
+ isAnnotationOverlay={true}
+ isAnnotationOverlayScrollable={true}
+ childDocumentsActive={this.props.isDocumentActive}
+ fieldKey={this.props.fieldKey + "-annotations"}
+ dropAction={"move"}
+ select={emptyFunction}
+ addDocument={this.addAnnotationDocument}
+ removeDocument={this.remAnnotationDocument}
+ moveDocument={this.moveAnnotationDocument}
+ bringToFront={emptyFunction}
+ renderDepth={this.props.renderDepth + 1} >
+ {this.contentFunc}
+ </CollectionFreeFormView> :
+ this.contentFunc();
}
} \ No newline at end of file
diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx
index a7ca57b0b..3ad9333de 100644
--- a/src/client/views/collections/CollectionView.tsx
+++ b/src/client/views/collections/CollectionView.tsx
@@ -63,6 +63,7 @@ export enum CollectionViewType {
}
export interface CollectionViewProps extends FieldViewProps {
isAnnotationOverlay?: boolean; // is the collection an annotation overlay (eg an overlay on an image/video/etc)
+ isAnnotationOverlayScrollable?: boolean; // whether the annotation overlay can be vertically scrolled (just for tree views, currently)
layoutEngine?: () => string;
setPreviewCursor?: (func: (x: number, y: number, drag: boolean, hide: boolean) => void) => void;
@@ -125,8 +126,9 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
}
screenToLocalTransform = () => this.props.renderDepth ? this.props.ScreenToLocalTransform() : this.props.ScreenToLocalTransform().scale(this.props.PanelWidth() / this.bodyPanelWidth());
- private SubView = (type: CollectionViewType, props: SubCollectionViewProps) => {
+ private renderSubView = (type: CollectionViewType | undefined, props: SubCollectionViewProps) => {
TraceMobx();
+ if (type === undefined) return null;
switch (type) {
default:
case CollectionViewType.Freeform: return <CollectionFreeFormView key="collview" {...props} />;
@@ -246,17 +248,13 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
childLayoutTemplate = () => this.props.childLayoutTemplate?.() || Cast(this.rootDoc.childLayoutTemplate, Doc, null);
@computed get childLayoutString() { return StrCast(this.rootDoc.childLayoutString); }
-
- @observable _subView: any = undefined;
-
isContentActive = (outsideReaction?: boolean) => {
- return this.props.isContentActive() ? true : false;
+ return this.props.isContentActive();
}
render() {
TraceMobx();
const props: SubCollectionViewProps = {
...this.props,
- SetSubView: action((subView: any) => this._subView = subView),
addDocument: this.addDocument,
moveDocument: this.moveDocument,
removeDocument: this.removeDocument,
@@ -273,7 +271,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab
return (<div className={"collectionView"} onContextMenu={this.onContextMenu}
style={{ pointerEvents: this.props.layerProvider?.(this.rootDoc) === false ? "none" : undefined }}>
{this.showIsTagged()}
- {this.collectionViewType !== undefined ? this.SubView(this.collectionViewType, props) : (null)}
+ {this.renderSubView(this.collectionViewType, props)}
</div>);
}
}
diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx
index 6c6a2fb05..7e57d0e89 100644
--- a/src/client/views/collections/TabDocView.tsx
+++ b/src/client/views/collections/TabDocView.tsx
@@ -476,7 +476,6 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
<div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}>
<CollectionFreeFormView
Document={this.props.document}
- SetSubView={() => this}
CollectionView={undefined}
ContainingCollectionView={undefined}
ContainingCollectionDoc={undefined}
@@ -484,7 +483,7 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> {
childLayoutTemplate={this.childLayoutTemplate} // bcz: Ugh .. should probably be rendering a CollectionView or the minimap should be part of the collectionFreeFormView to avoid having to set stuff like this.
noOverlay={true} // don't render overlay Docs since they won't scale
setHeight={returnFalse}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
isAnyChildContentActive={returnFalse}
select={emptyFunction}
dropAction={undefined}
diff --git a/src/client/views/collections/TreeView.scss b/src/client/views/collections/TreeView.scss
index 1ebc5873e..2e33d3564 100644
--- a/src/client/views/collections/TreeView.scss
+++ b/src/client/views/collections/TreeView.scss
@@ -53,14 +53,11 @@
}
}
+.treeView-container-outline-active
.treeView-container-active {
z-index: 100;
position: relative;
-
- .formattedTextbox-sidebar {
- background-color: #ffff001f !important;
- height: 500px !important;
- }
+ pointer-events: all;
}
.treeView-openRight {
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index 7f2128230..eedb353e3 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -81,7 +81,7 @@ export class TreeView extends React.Component<TreeViewProps> {
static _openLevelScript: Opt<ScriptField | undefined>;
private _header: React.RefObject<HTMLDivElement> = React.createRef();
private _tref = React.createRef<HTMLDivElement>();
- private _docRef: Opt<DocumentView>;
+ @observable _docRef: Opt<DocumentView>;
private _selDisposer: Opt<IReactionDisposer>;
private _editTitleScript: (() => ScriptField) | undefined;
private _openScript: (() => ScriptField) | undefined;
@@ -116,7 +116,8 @@ export class TreeView extends React.Component<TreeViewProps> {
@computed get childLinks() { return this.childDocList("links"); }
@computed get childAliases() { return this.childDocList("aliases"); }
@computed get childAnnos() { return this.childDocList(this.fieldKey + "-annotations"); }
- @computed get selected() { return SelectionManager.Views().lastElement()?.props.Document === this.props.document; }
+ @computed get selected() { return SelectionManager.IsSelected(this._docRef); }
+ // SelectionManager.Views().lastElement()?.props.Document === this.props.document; }
childDocList(field: string) {
const layout = Cast(Doc.LayoutField(this.doc), Doc, null);
@@ -125,7 +126,12 @@ export class TreeView extends React.Component<TreeViewProps> {
DocListCastOrNull(this.doc[field]); // otherwise use the document's data field
}
@undoBatch move = (doc: Doc | Doc[], target: Doc | undefined, addDoc: (doc: Doc | Doc[]) => boolean) => {
- return this.doc !== target && this.props.removeDoc?.(doc) === true && addDoc(doc);
+ if (this.doc !== target && addDoc !== returnFalse) { // bcz: this should all be running in a Temp undo batch instead of hackily testing for returnFalse
+ if (this.props.removeDoc?.(doc) === true) {
+ return addDoc(doc);
+ }
+ }
+ return false;
}
@undoBatch @action remove = (doc: Doc | Doc[], key: string) => {
this.props.treeView.props.select(false);
@@ -141,8 +147,10 @@ export class TreeView extends React.Component<TreeViewProps> {
this._editTitle = false;
}
else if (docView.isSelected()) {
+ const doc = docView.Document;
+ SelectionManager.SelectSchemaViewDoc(doc);
this._editTitle = true;
- this._selDisposer = reaction(() => docView.isSelected(), sel => !sel && this.setEditTitle(undefined));
+ this._selDisposer = reaction(() => SelectionManager.SelectedSchemaDoc(), seldoc => seldoc !== doc && this.setEditTitle(undefined));
} else {
docView.select(false);
}
@@ -213,16 +221,18 @@ export class TreeView extends React.Component<TreeViewProps> {
const before = pt[1] < rect.top + rect.height / 2;
const inside = pt[0] > Math.min(rect.left + 75, rect.left + rect.width * .75) || (!before && this.treeViewOpen && this.childDocList.length);
this._header.current!.className = "treeView-header";
- if (inside) this._header.current!.className += " treeView-header-inside";
- else if (before) this._header.current!.className += " treeView-header-above";
- else if (!before) this._header.current!.className += " treeView-header-below";
+ if (!this.props.treeView.outlineMode || DragManager.DocDragData?.treeViewDoc === this.props.treeView.rootDoc) {
+ if (inside) this._header.current!.className += " treeView-header-inside";
+ else if (before) this._header.current!.className += " treeView-header-above";
+ else if (!before) this._header.current!.className += " treeView-header-below";
+ }
e.stopPropagation();
}
public static makeTextBullet() {
const bullet = Docs.Create.TextDocument("-text-", {
layout: CollectionView.LayoutString("data"),
- title: "-title-", "sidebarColor": "transparent", "sidebarViewType": CollectionViewType.Freeform,
+ title: "-title-",
treeViewExpandedViewLock: true, treeViewExpandedView: "data",
_viewType: CollectionViewType.Tree, hideLinkButton: true, _showSidebar: true, treeViewType: "outline",
x: 0, y: 0, _xMargin: 0, _yMargin: 0, _autoHeight: true, _singleLine: true, backgroundColor: "transparent", _width: 1000, _height: 10
@@ -244,9 +254,7 @@ export class TreeView extends React.Component<TreeViewProps> {
TreeView._editTitleOnLoad = { id: folder[Id], parent: this.props.parentTreeView };
return this.props.addDocument(folder);
}
- deleteFolder = () => {
- return this.props.removeDoc?.(this.doc);
- }
+ deleteItem = () => this.props.removeDoc?.(this.doc);
preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
const dragData = de.complete.docDragData;
@@ -266,23 +274,25 @@ export class TreeView extends React.Component<TreeViewProps> {
e.stopPropagation();
}
const docDragData = de.complete.docDragData;
- if (docDragData) {
- e.stopPropagation();
+ if (docDragData && pt[0] < rect.left + rect.width) {
if (docDragData.draggedDocuments[0] === this.doc) return true;
- this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document);
+ if (this.dropDocuments(docDragData.droppedDocuments, before, inside, docDragData.dropAction, docDragData.moveDocument, docDragData.treeViewDoc === this.props.treeView.props.Document)) {
+ e.stopPropagation();
+ }
}
}
dropDocuments(droppedDocuments: Doc[], before: boolean, inside: number | boolean, dropAction: dropActionType, moveDocument: DragManager.MoveFunction | undefined, forceAdd: boolean) {
const parentAddDoc = (doc: Doc | Doc[]) => this.props.addDocument(doc, undefined, before);
- const canAdd = !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add") || forceAdd;
+ const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.containerCollection)?.freezeChildren).includes("add")) || forceAdd;
const localAdd = (doc: Doc) => Doc.AddDocToList(this.dataDoc, this.fieldKey, doc) && ((doc.context = this.doc.context) || true) ? true : false;
const addDoc = !inside ? parentAddDoc :
(doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc), true as boolean);
const move = (!dropAction || dropAction === "proto" || dropAction === "move" || dropAction === "same") && moveDocument;
if (canAdd) {
- UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false));
+ return UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === "proto" ? addDoc(d) : false) : addDoc(d)) || added, false));
}
+ return false;
}
refTransform = (ref: HTMLDivElement | undefined | null) => {
@@ -432,7 +442,7 @@ export class TreeView extends React.Component<TreeViewProps> {
</div>
</ul>;
}
- return <ul>{this.renderEmbeddedDocument(false)}</ul>; // "layout"
+ return <ul onPointerDown={e => { e.preventDefault(); e.stopPropagation(); }}>{this.renderEmbeddedDocument(false, returnFalse)}</ul>; // "layout"
}
get onCheckedClick() { return this.doc.type === DocumentType.COL ? undefined : this.props.onCheckedClick?.() ?? ScriptCast(this.doc.onCheckedClick); }
@@ -519,16 +529,16 @@ export class TreeView extends React.Component<TreeViewProps> {
}
contextMenuItems = () => {
const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "New Folder" };
- const deleteFolder = { script: ScriptField.MakeFunction(`scriptContext.deleteFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "Delete Folder" };
- const folderOp = this.childDocs?.length ? makeFolder : deleteFolder;
+ const deleteItem = { script: ScriptField.MakeFunction(`scriptContext.deleteItem()`, { scriptContext: "any" })!, icon: "folder-plus", label: "Delete" };
+ const folderOp = this.childDocs?.length ? [makeFolder] : [];
const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, icon: "copy", label: "Open Alias" };
const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: "eye", label: "Focus or Open" };
- return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? [folderOp] :
+ return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? folderOp :
Doc.IsSystem(this.doc) ? [] :
this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ?
[openAlias, makeFolder] :
this.doc.viewType === CollectionViewType.Docking ? [] :
- [openAlias, focusDoc])];
+ [deleteItem, openAlias, focusDoc])];
}
childContextMenuItems = () => {
const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
@@ -581,6 +591,7 @@ export class TreeView extends React.Component<TreeViewProps> {
}
titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth() - 2 * treeBulletWidth()));
+ return18 = () => 18;
/**
* Renders the EditableView title element for placement into the tree.
*/
@@ -636,10 +647,10 @@ export class TreeView extends React.Component<TreeViewProps> {
moveDocument={this.move}
removeDocument={this.props.removeDoc}
ScreenToLocalTransform={this.getTransform}
- NativeHeight={() => 18}
+ NativeHeight={this.return18}
NativeWidth={this.titleWidth}
PanelWidth={this.titleWidth}
- PanelHeight={() => 18}
+ PanelHeight={this.return18}
contextMenuItems={this.contextMenuItems}
renderDepth={1}
isContentActive={this.props.isContentActive}
@@ -679,6 +690,7 @@ export class TreeView extends React.Component<TreeViewProps> {
renderBulletHeader = (contents: JSX.Element, editing: boolean) => {
return <>
<div className={`treeView-header` + (editing ? "-editing" : "")} key="titleheader"
+ style={{ width: "max-content" }}
ref={this._header}
onClick={this.ignoreEvent}
onPointerDown={this.ignoreEvent}
@@ -691,7 +703,7 @@ export class TreeView extends React.Component<TreeViewProps> {
}
- renderEmbeddedDocument = (asText: boolean) => {
+ renderEmbeddedDocument = (asText: boolean, isActive: () => boolean | undefined) => {
const layout = StrCast(Doc.LayoutField(this.layoutDoc));
const isExpandable = layout.includes(FormattedTextBox.name) || layout.includes(SliderBox.name);
const panelWidth = asText || isExpandable ? this.rtfWidth : this.expandPanelWidth;
@@ -704,8 +716,8 @@ export class TreeView extends React.Component<TreeViewProps> {
NativeWidth={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfWidth : undefined}
NativeHeight={!asText && (this.layoutDoc.type === DocumentType.RTF || this.layoutDoc.type === DocumentType.SLIDER) ? this.rtfHeight : undefined}
LayoutTemplateString={asText ? FormattedTextBox.LayoutString("text") : undefined}
- isContentActive={asText ? this.props.isContentActive : returnFalse}
- isDocumentActive={asText ? this.props.isContentActive : returnFalse}
+ isContentActive={isActive}
+ isDocumentActive={isActive}
styleProvider={asText ? this.titleStyleProvider : this.embeddedStyleProvider}
hideTitle={asText}
fitContentsToDoc={returnTrue}
@@ -749,7 +761,7 @@ export class TreeView extends React.Component<TreeViewProps> {
@computed get renderDocumentAsHeader() {
return <>
{this.renderBullet}
- {this.renderEmbeddedDocument(true)}
+ {this.renderEmbeddedDocument(true, this.props.isContentActive)}
</>;
}
@@ -770,19 +782,19 @@ export class TreeView extends React.Component<TreeViewProps> {
const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, "copy", undefined, false));
}
+
render() {
TraceMobx();
const hideTitle = this.doc.treeViewHideHeader || this.props.treeView.outlineMode;
return this.props.renderedIds.indexOf(this.doc[Id]) !== -1 ? "<" + this.doc.title + ">" : // just print the title of documents we've previously rendered in this hierarchical path to avoid cycles
<div className={`treeView-container${this.props.isContentActive() ? "-active" : ""}`}
ref={this.createTreeDropTarget}
-
onDrop={this.onTreeDrop}
//onPointerDown={e => this.props.isContentActive(true) && SelectionManager.DeselectAll()} // bcz: this breaks entering a text filter in a filterBox since it deselects the filter's target document
onKeyDown={this.onKeyDown}>
<li className="collection-child">
{hideTitle && this.doc.type !== DocumentType.RTF ?
- this.renderEmbeddedDocument(false) :
+ this.renderEmbeddedDocument(false, returnFalse) :
this.renderBulletHeader(hideTitle ? this.renderDocumentAsHeader : this.renderTitleAsHeader, this._editTitle)}
</li>
</div>;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
index 9769453a0..9cc887e3d 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx
@@ -153,28 +153,44 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo
const atop = this.visibleY(adiv);
const btop = this.visibleY(bdiv);
if (!a.width || !b.width) return undefined;
+ const aDocBounds = (A.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 };
+ const bDocBounds = (B.props as any).DocumentView?.().getBounds() || { left: 0, right: 0, top: 0, bottom: 0 };
+ const acentX = (a.left + a.right) / 2;
+ const acentY = (a.top + a.bottom) / 2;
+ const bcentX = (b.left + b.right) / 2;
+ const bcentY = (b.top + b.bottom) / 2;
+ const pt1Arc = ((acentX - aDocBounds.left) > 0.1 && (aDocBounds.right - acentX) > 0.1) ||
+ ((acentY - aDocBounds.top) > 0.1 && (aDocBounds.bottom - acentY) > 0.1);
+ const pt2Arc = ((bcentX - bDocBounds.left) > 0.1 && (bDocBounds.right - bcentX) > 0.1) ||
+ ((bcentY - bDocBounds.top) > 0.1 && (bDocBounds.bottom - bcentY) > 0.1);
const atop2 = this.visibleY(adiv);
const btop2 = this.visibleY(bdiv);
const aleft = this.visibleX(adiv);
const bleft = this.visibleX(bdiv);
const clipped = aleft !== a.left || atop !== a.top || bleft !== b.left || btop !== b.top;
- const apt = Utils.closestPtBetweenRectangles(aleft, atop, a.width, a.height, bleft, btop, b.width, b.height, a.left + a.width / 2, a.top + a.height / 2);
- const bpt = Utils.closestPtBetweenRectangles(bleft, btop, b.width, b.height, aleft, atop, a.width, a.height, apt.point.x, apt.point.y);
- const pt1 = [apt.point.x, apt.point.y];
- const pt2 = [bpt.point.x, bpt.point.y];
- const pt1vec = [pt1[0] - (aleft + a.width / 2), pt1[1] - (atop + a.height / 2)];
- const pt2vec = [pt2[0] - (bleft + b.width / 2), pt2[1] - (btop + b.height / 2)];
+ const pt1 = [aleft + a.width / 2, atop + a.height / 2];
+ const pt2 = [bleft + b.width / 2, btop + b.width / 2];
+ const pt1vec = [pt1[0] - (aDocBounds.left + aDocBounds.right) / 2, pt1[1] - (aDocBounds.top + aDocBounds.bottom) / 2];
+ const pt2vec = [pt2[0] - (bDocBounds.left + bDocBounds.right) / 2, pt2[1] - (bDocBounds.top + bDocBounds.bottom) / 2];
const pt1len = Math.sqrt((pt1vec[0] * pt1vec[0]) + (pt1vec[1] * pt1vec[1]));
const pt2len = Math.sqrt((pt2vec[0] * pt2vec[0]) + (pt2vec[1] * pt2vec[1]));
const ptlen = Math.sqrt((pt1[0] - pt2[0]) * (pt1[0] - pt2[0]) + (pt1[1] - pt2[1]) * (pt1[1] - pt2[1])) / 2;
- const pt1norm = clipped ? [0, 0] : [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen];
- const pt2norm = clipped ? [0, 0] : [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen];
+ const pt1norm = clipped ? [0, 0] : !pt1Arc ? [pt1vec[0] / pt1len * ptlen, pt1vec[1] / pt1len * ptlen] :
+ Math.abs(acentY - aDocBounds.top) < 0.01 ||
+ Math.abs(acentY - aDocBounds.bottom) < 0.01 ? [0, (pt2[1] - pt1[1]) / 2] : [(pt2[0] - pt1[0]) / 2, 0];
+ const pt2norm = clipped ? [0, 0] : !pt2Arc ? [pt2vec[0] / pt2len * ptlen, pt2vec[1] / pt2len * ptlen] :
+ Math.abs(bcentY - bDocBounds.top) < 0.01 ||
+ Math.abs(bcentY - bDocBounds.bottom) < 0.01 ? [0, (pt1[1] - pt2[1]) / 2] : [(pt1[0] - pt2[0]) / 2, 0];
+ const pt1normlen = Math.sqrt(pt1norm[0] * pt1norm[0] + pt1norm[1] * pt1norm[1]) || 1;
+ const pt2normlen = Math.sqrt(pt2norm[0] * pt2norm[0] + pt2norm[1] * pt2norm[1]) || 1;
+ const pt1normalized = [pt1norm[0] / pt1normlen, pt1norm[1] / pt1normlen];
+ const pt2normalized = [pt2norm[0] / pt2normlen, pt2norm[1] / pt2normlen];
const aActive = A.isSelected() || Doc.IsBrushed(A.rootDoc);
const bActive = B.isSelected() || Doc.IsBrushed(B.rootDoc);
const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].linkOffsetX);
const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].linkOffsetY);
- return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 };
+ return { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1: [pt1[0] + pt1normalized[0] * 13, pt1[1] + pt1normalized[1] * 13], pt2: [pt2[0] + pt2normalized[0] * 13, pt2[1] + pt2normalized[1] * 13] };
}
render() {
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index febccbfcc..aeda71d01 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,11 +1,12 @@
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, ObservableMap } from "mobx";
+import { Bezier } from "bezier-js";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { computedFn } from "mobx-utils";
import { DateField } from "../../../../fields/DateField";
import { Doc, HeightSym, Opt, StrListCast, WidthSym } from "../../../../fields/Doc";
import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas";
import { Id } from "../../../../fields/FieldSymbols";
-import { InkData, InkField, InkTool } from "../../../../fields/InkField";
+import { InkData, InkField, InkTool, PointData, Segment } from "../../../../fields/InkField";
import { List } from "../../../../fields/List";
import { ObjectField } from "../../../../fields/ObjectField";
import { RichTextField } from "../../../../fields/RichTextField";
@@ -27,14 +28,15 @@ import { InteractionUtils } from "../../../util/InteractionUtils";
import { LinkManager } from "../../../util/LinkManager";
import { SearchUtil } from "../../../util/SearchUtil";
import { SelectionManager } from "../../../util/SelectionManager";
+import { ColorScheme } from "../../../util/SettingsManager";
import { SnappingManager } from "../../../util/SnappingManager";
import { Transform } from "../../../util/Transform";
-import { undoBatch } from "../../../util/UndoManager";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
import { COLLECTION_BORDER_WIDTH } from "../../../views/global/globalCssVariables.scss";
import { Timeline } from "../../animationtimeline/Timeline";
import { ContextMenu } from "../../ContextMenu";
-import { DocumentDecorations } from "../../DocumentDecorations";
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkingStroke";
+import { GestureOverlay } from "../../GestureOverlay";
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from "../../InkingStroke";
import { LightboxView } from "../../LightboxView";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView";
@@ -50,7 +52,6 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso
import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
import React = require("react");
-import { ColorScheme } from "../../../util/SettingsManager";
export const panZoomSchema = createSchema({
_panX: "number",
@@ -98,6 +99,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
private _layoutSizeData = observable.map<string, { width?: number, height?: number }>();
private _cachedPool: Map<string, PoolData> = new Map();
private _lastTap = 0;
+ private _batch: UndoManager.Batch | undefined = undefined;
private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }
private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; }
@@ -111,6 +113,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
@observable _pullDirection: string = "";
@observable _showAnimTimeline = false;
@observable _clusterSets: (Doc[])[] = [];
+ @observable _deleteList: DocumentView[] = [];
@observable _timelineRef = React.createRef<Timeline>();
@observable _marqueeRef = React.createRef<HTMLDivElement>();
@observable _keyframeEditing = false;
@@ -146,7 +149,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
return this.props.ScreenToLocalTransform().translate(-this.borderWidth, -this.borderWidth);
}
@computed get cachedGetTransform(): Transform {
- return this.getTransformOverlay().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
+ return this.getContainerTransform().translate(- this.cachedCenteringShiftX, - this.cachedCenteringShiftY).transform(this.cachedGetLocalTransform);
}
@action setKeyFrameEditing = (set: boolean) => this._keyframeEditing = set;
@@ -165,11 +168,10 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document._panX);
panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document._panY);
zoomScaling = () => (this.freeformData()?.scale ?? NumCast(this.Document[this.scaleFieldKey], 1));
- contentTransform = () => `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
+ contentTransform = () => !this.cachedCenteringShiftX && !this.cachedCenteringShiftY && this.zoomScaling() === 1 ? "" : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`;
getTransform = () => this.cachedGetTransform.copy();
getLocalTransform = () => this.cachedGetLocalTransform.copy();
getContainerTransform = () => this.cachedGetContainerTransform.copy();
- getTransformOverlay = () => this.getContainerTransform().translate(1, 1);
getActiveDocuments = () => this.childLayoutPairs.filter(pair => this.isCurrent(pair.layout)).map(pair => pair.layout);
isAnyChildContentActive = () => this.props.isAnyChildContentActive();
addLiveTextBox = (newBox: Doc) => {
@@ -221,7 +223,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
if (!de.embedKey && !this.ChildDrag && this.props.layerProvider?.(this.props.Document) !== false && this.props.Document._isGroup) return false;
if (!super.onInternalDrop(e, de)) return false;
const refDoc = docDragData.droppedDocuments[0];
- const [xpo, ypo] = this.getTransformOverlay().transformPoint(de.x, de.y);
+ const [xpo, ypo] = this.getContainerTransform().transformPoint(de.x, de.y);
const z = NumCast(refDoc.z);
const x = (z ? xpo : xp) - docDragData.offset[0];
const y = (z ? ypo : yp) - docDragData.offset[1];
@@ -433,25 +435,29 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
@action
onPointerDown = (e: React.PointerEvent): void => {
- if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) ||
- ([InkTool.Pen, InkTool.Highlighter].includes(CurrentUserUtils.SelectedTool))) {
- return;
- }
- this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
+ this._downX = this._lastX = e.pageX;
+ this._downY = this._lastY = e.pageY;
if (e.button === 0 && !e.altKey && !e.ctrlKey && this.props.isContentActive(true)) {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointerup", this.onPointerUp);
- // if not using a pen and in no ink mode
- if (CurrentUserUtils.SelectedTool === InkTool.None) {
- this._downX = this._lastX = e.pageX;
- this._downY = this._lastY = e.pageY;
- }
- // eraser plus anything else mode
- else {
- e.stopPropagation();
- e.preventDefault();
+ if (!e.nativeEvent.cancelBubble &&
+ !this.props.Document._isGroup && // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag
+ !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) &&
+ !InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) {
+ switch (CurrentUserUtils.SelectedTool) {
+ case InkTool.Highlighter:
+ 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.Eraser:
+ document.addEventListener("pointermove", this.onEraserMove);
+ document.addEventListener("pointerup", this.onEraserUp);
+ this._batch = UndoManager.StartBatch("collectionErase");
+ e.stopPropagation();
+ e.preventDefault();
+ break;
+ case InkTool.None:
+ this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ break;
+ }
}
}
}
@@ -592,7 +598,18 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
}
}
+ @action
+ onEraserUp = (e: PointerEvent): void => {
+ if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
+ document.removeEventListener("pointermove", this.onEraserMove);
+ document.removeEventListener("pointerup", this.onEraserUp);
+ this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc));
+ this._deleteList = [];
+ this._batch?.end();
+ }
+ }
+ @action
onPointerUp = (e: PointerEvent): void => {
if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
document.removeEventListener("pointermove", this.onPointerMove);
@@ -623,25 +640,159 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
this._lastY = e.clientY;
}
+ /**
+ * 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.
+ * However, if Shift is held, then no segmentation is done -- instead any intersected stroke is deleted in its entirety.
+ */
+ @action
+ onEraserMove = (e: PointerEvent) => {
+ const currPoint = { X: e.clientX, Y: e.clientY };
+ this.getEraserIntersections({ X: this._lastX, Y: this._lastY }, currPoint).forEach(intersect => {
+ if (!this._deleteList.includes(intersect.inkView)) {
+ this._deleteList.push(intersect.inkView);
+ SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.strokeWidth?.toString()) || "1");
+ SetActiveInkColor(StrCast(intersect.inkView.rootDoc.color?.toString()) || "black");
+ // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
+ !e.shiftKey && this.segmentInkStroke(intersect.inkView, intersect.t).forEach(segment =>
+ GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke,
+ 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[])));
+ // Lower ink opacity to give the user a visual indicator of deletion.
+ intersect.inkView.layoutDoc.opacity = 0.5;
+ }
+ });
+ this._lastX = currPoint.X;
+ this._lastY = currPoint.Y;
+
+ e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
+ e.preventDefault();
+ }
+
@action
onPointerMove = (e: PointerEvent): void => {
- if (this.props.Document._isGroup) return; // groups don't pan when dragged -- instead let the event go through to allow the group itself to drag
if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return;
if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
if (this.props.isContentActive(true)) e.stopPropagation();
} else if (!e.cancelBubble) {
- if (CurrentUserUtils.SelectedTool === InkTool.None) {
- if (this.tryDragCluster(e, this._hitCluster)) {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- }
- else this.pan(e);
+ if (this.tryDragCluster(e, this._hitCluster)) {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
}
+ else this.pan(e);
e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
e.preventDefault();
}
}
+ /**
+ * Determines if the Eraser tool has intersected with an ink stroke in the current freeform collection.
+ * @returns an array of tuples containing the intersected ink DocumentView and the t-value where it was intersected
+ */
+ getEraserIntersections = (lastPoint: { X: number, Y: number }, currPoint: { X: number, Y: number }) => {
+ const eraserMin = { X: Math.min(lastPoint.X, currPoint.X), Y: Math.min(lastPoint.Y, currPoint.Y) };
+ const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) };
+ return this.childDocs
+ .map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView))
+ .filter(inkView => inkView?.ComponentView instanceof InkingStroke)
+ .map(inkView => ({ inkViewBounds: inkView!.getBounds(), inkStroke: inkView!.ComponentView as InkingStroke, inkView: inkView! }))
+ .filter(({ inkViewBounds }) => inkViewBounds && // bounding box of eraser segment and ink stroke overlap
+ eraserMin.X <= inkViewBounds.right && eraserMin.Y <= inkViewBounds.bottom &&
+ eraserMax.X >= inkViewBounds.left && eraserMax.Y >= inkViewBounds.top)
+ .reduce((intersections, { inkStroke, inkView }) => {
+ const { inkData } = inkStroke.inkScaledData();
+ // Convert from screen space to ink space for the intersection.
+ const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint);
+ const currPointInkSpace = inkStroke.ptFromScreen(currPoint);
+ for (var i = 0; i < inkData.length - 3; i += 4) {
+ const intersects = Array.from(new Set(InkField.Segment(inkData, i).intersects({ // compute all unique intersections
+ p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
+ p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }
+ }) as (number | string)[])); // convert to more manageable union array type
+ // return tuples of the inkingStroke intersected, and the t value of the intersection
+ intersections.push(...intersects.map(t => ({ inkView, t: (+t) + Math.floor(i / 4) })));// convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve
+ }
+ return intersections;
+ }, [] as { t: number, inkView: DocumentView }[]);
+ }
+
+ /**
+ * Performs segmentation of the ink stroke - creates "segments" or subsections of the current ink stroke at points in which the
+ * ink stroke intersects any other ink stroke (including itself).
+ * @param ink The ink DocumentView intersected by the eraser.
+ * @param excludeT The index of the curve in the ink document that the eraser intersection occurred.
+ * @returns The ink stroke represented as a list of segments, excluding the segment in which the eraser intersection occurred.
+ */
+ @action
+ segmentInkStroke = (ink: DocumentView, excludeT: number): Segment[] => {
+ const segments: Segment[] = [];
+ var segment: Segment = [];
+ var startSegmentT = 0;
+ const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
+ // This iterates through all segments of the curve and splits them where they intersect another curve.
+ // if 'excludeT' is specified, then any segment containing excludeT will be skipped (ie, deleted)
+ for (var i = 0; i < inkData.length - 3; i += 4) {
+ const inkSegment = InkField.Segment(inkData, i);
+ // Getting all t-value intersections of the current curve with all other curves.
+ const tVals = this.getInkIntersections(i, ink, inkSegment).sort();
+ if (tVals.length) {
+ tVals.forEach((t, index) => {
+ const docCurveTVal = t + Math.floor(i / 4);
+ if (excludeT < startSegmentT || excludeT > docCurveTVal) {
+ const localStartTVal = startSegmentT - Math.floor(i / 4);
+ segment.push(inkSegment.split(localStartTVal < 0 ? 0 : localStartTVal, t));
+ segment.length && segments.push(segment);
+ }
+ // start a new segment from the intersection t value
+ segment = tVals.length - 1 === index ? [inkSegment.split(t).right] : [];
+ startSegmentT = docCurveTVal;
+ });
+ } else {
+ segment.push(inkSegment);
+ }
+ }
+ if (excludeT < startSegmentT || excludeT > (inkData.length / 4)) {
+ segment.length && segments.push(segment);
+ }
+ return segments;
+ }
+
+ /**
+ * Determines all possible intersections of the current curve of the intersected ink stroke with all other curves of all
+ * ink strokes in the current collection.
+ * @param i The index of the current curve within the inkData of the intersected ink stroke.
+ * @param ink The intersected DocumentView of the ink stroke.
+ * @param curve The current curve of the intersected ink stroke.
+ * @returns A list of all t-values at which intersections occur at the current curve of the intersected ink stroke.
+ */
+ getInkIntersections = (i: number, ink: DocumentView, curve: Bezier): number[] => {
+ const tVals: number[] = [];
+ // Iterating through all ink strokes in the current freeform collection.
+ this.childDocs
+ .filter(doc => doc.type === DocumentType.INK)
+ .forEach(doc => {
+ const otherInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)?.ComponentView as InkingStroke;
+ const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
+ const otherScreenPts = otherInkData.map(point => otherInk.ptToScreen(point));
+ const otherCtrlPts = otherScreenPts.map(spt => (ink.ComponentView as InkingStroke).ptFromScreen(spt));
+ for (var j = 0; j < otherCtrlPts.length - 3; j += 4) {
+ const neighboringSegment = i === j || i === j - 4 || i === j + 4;
+ // Ensuring that the curve intersected by the eraser is not checked for further ink intersections.
+ if (ink?.Document === otherInk.props.Document && neighboringSegment) continue;
+
+ const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y })));
+ curve.intersects(otherCurve).forEach((val: string | number, i: number) => {
+ // Converting the Bezier.js Split type to a t-value number.
+ const t = +val.toString().split("/")[0];
+ if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack! don't know why but intersection points are doubled from bezier.js (but not identical).
+ });
+ }
+ });
+ return tVals;
+ }
+
handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
if (!e.cancelBubble) {
const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
@@ -803,7 +954,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
else if (this.props.isContentActive(true) && !this.Document._isGroup) {
e.stopPropagation();
e.preventDefault();
- this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
+ !this.props.isAnnotationOverlayScrollable && this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc?
}
}
@@ -1010,13 +1161,13 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
rootSelected={childData ? this.rootSelected : returnFalse}
onClick={this.onChildClickHandler}
onDoubleClick={this.onChildDoubleClickHandler}
- ScreenToLocalTransform={childLayout.z ? this.getTransformOverlay : this.getTransform}
+ ScreenToLocalTransform={childLayout.z ? this.getContainerTransform : this.getTransform}
PanelWidth={childLayout[WidthSym]}
PanelHeight={childLayout[HeightSym]}
docFilters={this.childDocFilters}
docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
isDocumentActive={this.props.childDocumentsActive ? this.props.isDocumentActive : this.isContentActive}
focus={this.focusDocument}
addDocTab={this.addDocTab}
@@ -1035,7 +1186,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
bringToFront={this.bringToFront}
showTitle={this.props.childShowTitle}
dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView}
- pointerEvents={this.backgroundActive || this.props.childPointerEvents ? "all" :
+ pointerEvents={this.props.isContentActive() === false ? "none" : this.backgroundActive || this.props.childPointerEvents ? "all" :
(this.props.viewDefDivClick || (engine === "pass" && !this.props.isSelected(true))) ? "none" : this.props.pointerEvents}
jitterRotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0}
//fitToBox={this.props.fitToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this
@@ -1403,7 +1554,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
onPointerOver = (e: React.PointerEvent) => {
- (DocumentDecorations.Instance.Interacting || (this.props.layerProvider?.(this.props.Document) !== false && SnappingManager.GetIsDragging())) && this.setupDragLines(e.ctrlKey || e.shiftKey);
e.stopPropagation();
}
@@ -1476,6 +1626,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
{this.layoutDoc._backgroundGridShow ? this.backgroundGrid : (null)}
<CollectionFreeFormViewPannableContents
isAnnotationOverlay={this.isAnnotationOverlay}
+ isAnnotationOverlayScrollable={this.props.isAnnotationOverlayScrollable}
transform={this.contentTransform}
zoomScaling={this.zoomScaling}
presPaths={BoolCast(this.Document.presPathView)}
@@ -1584,6 +1735,7 @@ interface CollectionFreeFormViewPannableContentsProps {
progressivize?: boolean;
presPinView?: boolean;
isAnnotationOverlay: boolean | undefined;
+ isAnnotationOverlayScrollable: boolean | undefined;
}
@observer
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
index 65c345547..ec1cbadd5 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx
@@ -6,7 +6,7 @@ import { documentSchema } from '../../../../fields/documentSchemas';
import { List } from '../../../../fields/List';
import { makeInterface } from '../../../../fields/Schema';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { returnFalse, emptyPath, returnEmptyDoclist } from '../../../../Utils';
+import { returnFalse, emptyPath, returnEmptyDoclist, emptyFunction } from '../../../../Utils';
import { DragManager, dropActionType } from '../../../util/DragManager';
import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
@@ -228,7 +228,7 @@ export class CollectionMulticolumnView extends CollectionSubView(MulticolumnDocu
LayoutTemplateString={this.props.childLayoutString}
freezeDimensions={this.props.childFreezeDimensions}
renderDepth={this.props.renderDepth + 1}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
PanelWidth={width}
PanelHeight={height}
rootSelected={this.rootSelected}
diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
index 30836854a..a2d51e2e7 100644
--- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
+++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx
@@ -6,7 +6,7 @@ import { documentSchema } from '../../../../fields/documentSchemas';
import { List } from '../../../../fields/List';
import { makeInterface } from '../../../../fields/Schema';
import { BoolCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types';
-import { returnFalse, emptyPath, returnEmptyDoclist } from '../../../../Utils';
+import { returnFalse, emptyPath, returnEmptyDoclist, emptyFunction } from '../../../../Utils';
import { DragManager, dropActionType } from '../../../util/DragManager';
import { Transform } from '../../../util/Transform';
import { undoBatch } from '../../../util/UndoManager';
@@ -237,7 +237,7 @@ export class CollectionMultirowView extends CollectionSubView(MultirowDocument)
ScreenToLocalTransform={dxf}
focus={this.props.focus}
docFilters={this.childDocFilters}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
docRangeFilters={this.childDocRangeFilters}
searchFilterDocs={this.searchFilterDocs}
ContainingCollectionDoc={this.props.CollectionView?.props.Document}
diff --git a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
index 9fe18d118..273e609ca 100644
--- a/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MulticolumnResizer.tsx
@@ -11,7 +11,7 @@ import { StyleProp } from "../../StyleProvider";
interface ResizerProps {
width: number;
styleProvider?: StyleProviderFunc;
- isContentActive?: () => boolean;
+ isContentActive?: () => boolean | undefined;
columnUnitLength(): number | undefined;
toLeft?: Doc;
toRight?: Doc;
diff --git a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
index 5478bf709..006ef4df6 100644
--- a/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
+++ b/src/client/views/collections/collectionMulticolumn/MultirowResizer.tsx
@@ -11,7 +11,7 @@ import { StyleProviderFunc } from "../../nodes/DocumentView";
interface ResizerProps {
height: number;
styleProvider?: StyleProviderFunc;
- isContentActive?: () => boolean;
+ isContentActive?: () => boolean | undefined;
columnUnitLength(): number | undefined;
toTop?: Doc;
toBottom?: Doc;
diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
index 1306b79cb..dc35b5749 100644
--- a/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
+++ b/src/client/views/collections/collectionSchema/CollectionSchemaHeaders.tsx
@@ -225,7 +225,7 @@ export interface KeysDropdownProps {
fieldKey: string;
ContainingCollectionDoc: Doc | undefined;
ContainingCollectionView: Opt<CollectionView>;
- active?: (outsideReaction?: boolean) => boolean;
+ active?: (outsideReaction?: boolean) => boolean | undefined;
openHeader: (column: any, screenx: number, screeny: number) => void;
col: SchemaHeaderField;
icon: IconProp;
diff --git a/src/client/views/collections/collectionSchema/SchemaTable.tsx b/src/client/views/collections/collectionSchema/SchemaTable.tsx
index bc5a9559f..2219345f6 100644
--- a/src/client/views/collections/collectionSchema/SchemaTable.tsx
+++ b/src/client/views/collections/collectionSchema/SchemaTable.tsx
@@ -68,7 +68,7 @@ export interface SchemaTableProps {
addDocument?: (document: Doc | Doc[]) => boolean;
moveDocument?: (document: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[]) => boolean) => boolean;
ScreenToLocalTransform: () => Transform;
- active: (outsideReaction: boolean | undefined) => boolean;
+ active: (outsideReaction: boolean | undefined) => boolean | undefined;
onDrop: (e: React.DragEvent<Element>, options: DocumentOptions, completed?: (() => void) | undefined) => void;
addDocTab: (document: Doc, where: string) => boolean;
pinToPres: (document: Doc) => void;
diff --git a/src/client/views/nodes/DocumentView.scss b/src/client/views/nodes/DocumentView.scss
index 1ec7bf72a..9fcd45e72 100644
--- a/src/client/views/nodes/DocumentView.scss
+++ b/src/client/views/nodes/DocumentView.scss
@@ -175,7 +175,7 @@
position: absolute;
bottom: 0;
width: 100%;
- overflow-y: scroll;
+ overflow-y: auto;
transform-origin: bottom left;
opacity: 0.1;
transition: opacity 0.5s;
diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx
index 5d33f6b1c..138bad9b8 100644
--- a/src/client/views/nodes/DocumentView.tsx
+++ b/src/client/views/nodes/DocumentView.tsx
@@ -138,6 +138,7 @@ export interface DocumentViewSharedProps {
hideLinkButton?: boolean;
hideCaptions?: boolean;
ignoreAutoHeight?: boolean;
+ forceAutoHeight?: boolean;
disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over.
pointerEvents?: string;
scriptContext?: any; // can be assigned anything and will be passed as 'scriptContext' to any OnClick script that executes on this document
@@ -224,7 +225,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
componentWillUnmount() { this.cleanupHandlers(true); }
componentDidMount() { this.setupHandlers(); }
- componentDidUpdate() { this.setupHandlers(); }
+ //componentDidUpdate() { this.setupHandlers(); }
setupHandlers() {
this.cleanupHandlers(false);
if (this._mainCont.current) {
@@ -415,6 +416,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
ffview && runInAction(() => (ffview.ChildDrag = this.props.DocumentView()));
DragManager.StartDocumentDrag([this._mainCont.current], dragData, x, y, { hideSource: hideSource || (!dropAction && !this.layoutDoc.onDragStart) },
() => setTimeout(action(() => ffview && (ffview.ChildDrag = undefined)))); // this needs to happen after the drop event is processed.
+ ffview?.setupDragLines(false);
}
}
@@ -824,13 +826,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps
}
setContentView = action((view: { getAnchor?: () => Doc, forward?: () => boolean, back?: () => boolean }) => this._componentView = view);
isContentActive = (outsideReaction?: boolean) => {
- return (CurrentUserUtils.SelectedTool !== InkTool.None ||
+ return this.props.isContentActive() === false ? false : (
+ CurrentUserUtils.SelectedTool !== InkTool.None ||
SnappingManager.GetIsDragging() ||
this.rootSelected() ||
this.props.Document.forceActive ||
this.props.isSelected(outsideReaction) ||
this._componentView?.isAnyChildContentActive?.() ||
- this.props.isContentActive() ? true : false);
+ this.props.isContentActive()) ? true : undefined;
}
@computed get contents() {
TraceMobx();
@@ -1255,7 +1258,7 @@ export class DocumentView extends React.Component<DocumentViewProps> {
position: this.props.Document.isInkMask ? "absolute" : undefined,
transform: isButton ? undefined : `translate(${this.centeringX}px, ${this.centeringY}px)`,
width: isButton ? "100%" : xshift() ?? `${100 * (this.props.PanelWidth() - this.Xshift * 2) / this.props.PanelWidth()}%`,
- height: isButton ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` :
+ height: isButton || this.props.forceAutoHeight ? undefined : yshift() ?? (this.fitWidth ? `${this.panelHeight}px` :
`${100 * this.effectiveNativeHeight / this.effectiveNativeWidth * this.props.PanelWidth() / this.props.PanelHeight()}%`),
}}>
<DocumentViewInternal {...this.props}
diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx
index ee81e106a..943b9f153 100644
--- a/src/client/views/nodes/FieldView.tsx
+++ b/src/client/views/nodes/FieldView.tsx
@@ -18,7 +18,7 @@ export interface FieldViewProps extends DocumentViewSharedProps {
scrollOverflow?: boolean; // bcz: would like to think this can be avoided -- need to look at further
select: (isCtrlPressed: boolean) => void;
- isContentActive: (outsideReaction?: boolean) => boolean;
+ isContentActive: (outsideReaction?: boolean) => boolean | undefined;
isDocumentActive?: () => boolean;
isSelected: (outsideReaction?: boolean) => boolean;
scaling?: () => number;
diff --git a/src/client/views/nodes/FilterBox.tsx b/src/client/views/nodes/FilterBox.tsx
index 2041c7399..fb8e89da9 100644
--- a/src/client/views/nodes/FilterBox.tsx
+++ b/src/client/views/nodes/FilterBox.tsx
@@ -225,7 +225,7 @@ export class FilterBox extends ViewBoxBaseComponent<FieldViewProps, FilterBoxDoc
treeViewExpandedView: "layout", _treeViewOpen: true, _forceActive: true, ignoreClick: true
});
Doc.GetProto(newFacet).type = DocumentType.COL; // forces item to show an open/close button instead ofa checkbox
- newFacet._textBoxPadding = 4;
+ newFacet._textBoxPaddingX = newFacet._textBoxPaddingY = 4;
const scriptText = `setDocFilter(this?.target, "${facetHeader}", text, "match")`;
newFacet.onTextChanged = ScriptField.MakeScript(scriptText, { this: Doc.name, text: "string" });
} else if (facetHeader !== "tags" && nonNumbers / facetValues.strings.length < .1) {
diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx
index b82d16677..879a63248 100644
--- a/src/client/views/nodes/LinkBox.tsx
+++ b/src/client/views/nodes/LinkBox.tsx
@@ -15,7 +15,7 @@ const LinkDocument = makeInterface(documentSchema);
@observer
export class LinkBox extends ViewBoxBaseComponent<FieldViewProps, LinkDocument>(LinkDocument) {
public static LayoutString(fieldKey: string) { return FieldView.LayoutString(LinkBox, fieldKey); }
- isContentActiveFunc = () => this.isContentActive() ? true : false;
+ isContentActiveFunc = () => this.isContentActive();
render() {
if (this.dataDoc.treeViewOpen === undefined) setTimeout(() => this.dataDoc.treeViewOpen = true);
return <div className={`linkBox-container${this.isContentActive() ? "-interactive" : ""}`}
diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx
index 424083dac..2e29c0656 100644
--- a/src/client/views/nodes/LinkDocPreview.tsx
+++ b/src/client/views/nodes/LinkDocPreview.tsx
@@ -166,7 +166,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> {
docViewPath={returnEmptyDoclist}
ScreenToLocalTransform={Transform.Identity}
isDocumentActive={returnFalse}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
addDocument={returnFalse}
removeDocument={returnFalse}
addDocTab={returnFalse}
diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx
index 7ad96bf05..0c631e5f9 100644
--- a/src/client/views/nodes/ScreenshotBox.tsx
+++ b/src/client/views/nodes/ScreenshotBox.tsx
@@ -313,7 +313,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl
PanelHeight={this.formattedPanelHeight}
isAnnotationOverlay={true}
select={emptyFunction}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
scaling={returnOne}
xPadding={25}
yPadding={10}
diff --git a/src/client/views/nodes/VideoBox.scss b/src/client/views/nodes/VideoBox.scss
index 4871599b8..f0d7bd2f3 100644
--- a/src/client/views/nodes/VideoBox.scss
+++ b/src/client/views/nodes/VideoBox.scss
@@ -48,10 +48,18 @@
width: 100%;
z-index: -1; // 0; // logically this should be 0 (or unset) which would give us transparent brush strokes over videos. However, this makes Chrome crawl to a halt
position: absolute;
+ video {
+ width: auto;
+ height: 100%;
+ display: flex;
+ margin: auto;
+ }
}
.videoBox-content, .videoBox-content-interactive, .videoBox-content-fullScreen {
- height: Auto;
+ width: 100%;
+ height: 100%;
+ left: 0px;
}
.videoBox-content-YouTube, .videoBox-content-YouTube-fullScreen {
diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx
index 440ccf638..615d595c0 100644
--- a/src/client/views/nodes/VideoBox.tsx
+++ b/src/client/views/nodes/VideoBox.tsx
@@ -307,12 +307,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get content() {
const field = Cast(this.dataDoc[this.fieldKey], VideoField);
const interactive = CurrentUserUtils.SelectedTool !== InkTool.None || !this.props.isSelected() ? "" : "-interactive";
- const style = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
+ const classname = "videoBox-content" + (this._fullScreen ? "-fullScreen" : "") + interactive;
return !field ? <div key="loading">Loading</div> :
- <div className="container" key="container" style={{ mixBlendMode: "multiply", pointerEvents: this.props.isContentActive() ? "all" : "none" }}>
- <div className={`${style}`} style={{ width: "100%", height: "100%", left: "0px" }}>
+ <div className="videoBox-contentContainer" key="container" style={{ mixBlendMode: "multiply" }}>
+ <div className={classname}>
<video key="video" autoPlay={this._screenCapture} ref={this.setVideoRef}
- style={{ height: "100%", width: "auto", display: "flex", margin: "auto" }}
onCanPlay={this.videoLoad}
controls={VideoBox._nativeControls}
onPlay={() => this.Play()}
@@ -457,11 +456,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp
@computed get youtubeContent() {
this._youtubeIframeId = VideoBox._youtubeIframeCounter++;
this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true;
- const style = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
+ const classname = "videoBox-content-YouTube" + (this._fullScreen ? "-fullScreen" : "");
const start = untracked(() => Math.round((this.layoutDoc._currentTimecode || 0)));
return <iframe key={this._youtubeIframeId} id={`${this.youtubeVideoId + this._youtubeIframeId}-player`}
onPointerLeave={this.updateTimecode}
- onLoad={this.youtubeIframeLoaded} className={`${style}`} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
+ onLoad={this.youtubeIframeLoaded} className={classname} width={Doc.NativeWidth(this.layoutDoc) || 640} height={Doc.NativeHeight(this.layoutDoc) || 390}
src={`https://www.youtube.com/embed/${this.youtubeVideoId}?enablejsapi=1&rel=0&showinfo=1&autoplay=0&mute=1&start=${start}&modestbranding=1&controls=${VideoBox._nativeControls ? 1 : 0}`} />;
}
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 33fa23805..14b1cbb5d 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -766,7 +766,7 @@ Scripting.addGlobal(function setActiveInkTool(tool: string, checkResult?: boolea
Doc.UserDoc().activeInkTool = InkTool.Pen;
GestureOverlay.Instance.InkShape = tool;
}
- } else if (tool) { // pen
+ } else if (tool) { // pen or eraser
if (Doc.UserDoc().activeInkTool === tool && !GestureOverlay.Instance.InkShape) {
Doc.UserDoc().activeInkTool = InkTool.None;
} else {
diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
index cfbd1962e..e61f96852 100644
--- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx
+++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx
@@ -124,7 +124,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
@computed get sidebarWidthPercent() { return this._showSidebar ? "20%" : StrCast(this.layoutDoc._sidebarWidthPercent, "0%"); }
@computed get sidebarColor() { return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + "-backgroundColor"], "#e4e4e4")); }
- @computed get autoHeight() { return this.layoutDoc._autoHeight && !this.props.ignoreAutoHeight; }
+ @computed get autoHeight() { return (this.props.forceAutoHeight || this.layoutDoc._autoHeight) && !this.props.ignoreAutoHeight; }
@computed get textHeight() { return NumCast(this.rootDoc[this.fieldKey + "-height"]); }
@computed get scrollHeight() { return NumCast(this.rootDoc[this.fieldKey + "-scrollHeight"]); }
@computed get sidebarHeight() { return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + "-height"]); }
@@ -1141,10 +1141,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
clipboardTextSerializer: this.clipboardTextSerializer,
handlePaste: this.handlePaste,
});
- const startupText = !rtfField && this._editorView && Field.toString(this.dataDoc[fieldKey] as Field);
- if (startupText) {
- const { state: { tr }, dispatch } = this._editorView;
- dispatch(tr.insertText(startupText));
+ const { state, dispatch } = this._editorView;
+ if (!rtfField) {
+ const startupText = Field.toString(this.dataDoc[fieldKey] as Field);
+ if (startupText) {
+ dispatch(state.tr.insertText(startupText));
+ } else if (!FormattedTextBox.LiveTextUndo) {
+ selectAll(this._editorView.state, (tr) => {
+ this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: "center" })));
+ });
+ }
}
(this._editorView as any).TextView = this;
}
@@ -1175,8 +1181,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
...(Doc.UserDoc().fontColor !== "transparent" && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []),
...(Doc.UserDoc().fontStyle === "italics" ? [schema.mark(schema.marks.em)] : []),
...(Doc.UserDoc().textDecoration === "underline" ? [schema.mark(schema.marks.underline)] : []),
- ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: StrCast(Doc.UserDoc().fontFamily) })] : []),
- ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: StrCast(Doc.UserDoc().fontSize, "") })] : []),
+ ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []),
+ ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []),
...(Doc.UserDoc().fontWeight === "bold" ? [schema.mark(schema.marks.strong)] : [])];
}
}
@@ -1570,7 +1576,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
setHeight={this.setSidebarHeight}
fitContentsToDoc={this.fitToBox}
noSidebar={true}
- fieldKey={this.layoutDoc.sidebarViewType === "translation" ? `${this.fieldKey}-translation` : this.SidebarKey} />;
+ fieldKey={this.layoutDoc.sidebarViewType === "translation" ? `${this.fieldKey}-translation` : `${this.fieldKey}-annotations`} />;
};
return <div className={"formattedTextBox-sidebar" + (CurrentUserUtils.SelectedTool !== InkTool.None ? "-inking" : "")}
style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}>
@@ -1586,10 +1592,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
const interactive = (CurrentUserUtils.SelectedTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || this.props.layerProvider?.(this.layoutDoc) !== false);
if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide);
const minimal = this.props.ignoreAutoHeight;
- const margins = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
- const selPad = Math.min(margins, 10);
- const padding = Math.max(margins + ((selected && !this.layoutDoc._singleLine) || minimal ? -selPad : 0), 0);
- const selPaddingClass = selected && !this.layoutDoc._singleLine && margins >= 10 ? "-selected" : "";
+ const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0);
+ const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0);
+ const selPad = ((selected && !this.layoutDoc._singleLine) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0);
+ const selPaddingClass = selected && !this.layoutDoc._singleLine && paddingY >= 10 ? "-selected" : "";
const styleFromString = this.styleFromLayoutString(scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' >
return (styleFromString?.height === "0px" ? (null) :
<div className="formattedTextBox-cont"
@@ -1633,7 +1639,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<(FieldViewProp
onScroll={this.onScroll} onDrop={this.ondrop} >
<div className={minimal ? "formattedTextBox-minimal" : `formattedTextBox-inner${rounded}${selPaddingClass}`} ref={this.createDropTarget}
style={{
- padding: StrCast(this.layoutDoc._textBoxPadding, `${padding}px`),
+ padding: StrCast(this.layoutDoc._textBoxPadding),
+ paddingLeft: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
+ paddingRight: StrCast(this.layoutDoc._textBoxPaddingX, `${paddingX - selPad}px`),
+ paddingTop: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
+ paddingBottom: StrCast(this.layoutDoc._textBoxPaddingY, `${paddingY - selPad}px`),
pointerEvents: !active && !SnappingManager.GetIsDragging() ? (this.layoutDoc.isLinkButton ? "none" : undefined) : undefined
}}
/>
diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx
index 3612bd7c4..09cfb2077 100644
--- a/src/client/views/search/SearchBox.tsx
+++ b/src/client/views/search/SearchBox.tsx
@@ -1,7 +1,7 @@
import { action, computed, observable } from 'mobx';
import { observer } from 'mobx-react';
import * as React from 'react';
-import { Doc, DocListCast, DocListCastAsync, Field } from '../../../fields/Doc';
+import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field } from '../../../fields/Doc';
import { documentSchema } from "../../../fields/documentSchemas";
import { Id } from '../../../fields/FieldSymbols';
import { createSchema, makeInterface } from '../../../fields/Schema';
@@ -14,6 +14,8 @@ import "./SearchBox.scss";
import { DocumentManager } from '../../util/DocumentManager';
import { DocUtils } from '../../documents/Documents';
import { Tooltip } from "@material-ui/core";
+import { DictationOverlay } from '../DictationOverlay';
+import { CollectionSchemaBooleanCell } from '../collections/collectionSchema/CollectionSchemaCells';
export const searchSchema = createSchema({
Document: Doc
@@ -22,6 +24,10 @@ export const searchSchema = createSchema({
type SearchBoxDocument = makeInterface<[typeof documentSchema, typeof searchSchema]>;
const SearchBoxDocument = makeInterface(documentSchema, searchSchema);
+const DAMPENING_FACTOR = 0.9;
+const MAX_ITERATIONS = 25;
+const ERROR = 0.03;
+
export interface SearchBoxProps extends FieldViewProps {
linkSearch: boolean;
linkFrom?: (() => Doc | undefined) | undefined;
@@ -40,7 +46,10 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
@observable _searchString = "";
@observable _docTypeString = "all";
- @observable _results: [Doc, string[]][] = [];
+ @observable _results: Map<Doc, string[]> = new Map<Doc, string[]>();
+ @observable _pageRanks: Map<Doc, number> = new Map<Doc, number>();
+ @observable _linkedDocsOut: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>();
+ @observable _linkedDocsIn: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>();
@observable _selectedResult: Doc | undefined = undefined;
@observable _deletedDocsStatus: boolean = false;
@observable _onlyAliases: boolean = true;
@@ -110,11 +119,9 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
});
makeLink = action((linkTo: Doc) => {
- console.log(linkTo.title);
if (this.props.linkFrom) {
const linkFrom = this.props.linkFrom();
if (linkFrom) {
- console.log(linkFrom.title);
DocUtils.MakeLink({ doc: linkFrom }, { doc: linkTo }, "Link");
}
}
@@ -204,7 +211,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
const collection = CollectionDockingView.Instance;
query = query.toLowerCase();
- this._results = [];
+ this._results.clear();
this._selectedResult = undefined;
if (collection !== undefined) {
@@ -216,16 +223,114 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
const hlights = new Set<string>();
SearchBox.documentKeys(doc).forEach(key => Field.toString(doc[key] as Field).toLowerCase().includes(query) && hlights.add(key));
blockedKeys.forEach(key => hlights.delete(key));
- Array.from(hlights.keys()).length > 0 && this._results.push([doc, Array.from(hlights.keys())]);
+
+ if (Array.from(hlights.keys()).length > 0) {
+ this._results.set(doc, Array.from(hlights.keys()));
+ }
}
docIDs.push(doc[Id]);
});
}
+
+ this.computePageRanks();
+ }
+
+ /**
+ * This method initializes the page rank of every document to the reciprocal
+ * of the number of documents in the collection.
+ */
+ @action
+ initializePageRanks() {
+ this._pageRanks.clear();
+ this._linkedDocsOut.clear();
+
+ this._results.forEach((_, doc) => {
+ this._linkedDocsIn.set(doc, new Set());
+ });
+
+ this._results.forEach((_, doc) => {
+ this._pageRanks.set(doc, 1.0 / this._results.size);
+
+ if (Doc.GetProto(doc)[DirectLinksSym].size === 0) {
+ this._linkedDocsOut.set(doc, new Set(this._results.keys()));
+
+ this._results.forEach((_, linkedDoc) => {
+ this._linkedDocsIn.get(linkedDoc)?.add(doc);
+ });
+ }
+ else {
+ const linkedDocSet = new Set<Doc>();
+
+ Doc.GetProto(doc)[DirectLinksSym].forEach((link) => {
+ const d1 = link?.anchor1 as Doc;
+ const d2 = link?.anchor2 as Doc;
+ if (doc === d1 && this._results.has(d2)) {
+ linkedDocSet.add(d2);
+ this._linkedDocsIn.get(d2)?.add(doc);
+ }
+ else if (doc === d2 && this._results.has(d1)) {
+ linkedDocSet.add(d1);
+ this._linkedDocsIn.get(d1)?.add(doc);
+ }
+ });
+
+ this._linkedDocsOut.set(doc, linkedDocSet);
+ }
+ });
+ }
+
+ /**
+ * This method runs one complete iteration of the page rank algorithm. It
+ * returns true iff all page ranks have converged (i.e. changed by less than
+ * the _error value), which means that the algorithm should terminate.
+ *
+ * @return true if page ranks have converged; false otherwise
+ */
+ @action
+ pageRankIteration(): boolean {
+ let converged = true;
+ const pageRankFromAll = (1 - DAMPENING_FACTOR) / this._results.size;
+
+ const nextPageRanks = new Map<Doc, number>();
+
+ this._results.forEach((_, doc) => {
+ let nextPageRank = pageRankFromAll;
+
+ this._linkedDocsIn.get(doc)?.forEach((linkedDoc) => {
+ nextPageRank += DAMPENING_FACTOR * (this._pageRanks.get(linkedDoc) ?? 0) / (this._linkedDocsOut.get(linkedDoc)?.size ?? 1);
+ });
+
+ nextPageRanks.set(doc, nextPageRank);
+
+ if (Math.abs(nextPageRank - (this._pageRanks.get(doc) ?? 0)) > ERROR) {
+ converged = false;
+ }
+ });
+
+ this._pageRanks = nextPageRanks;
+
+ return converged;
+ }
+
+ /**
+ * This method performs the page rank algorithm on the graph of documents
+ * that match the search query. Vertices are documents and edges are links
+ * between documents.
+ */
+ @action
+ computePageRanks() {
+ this.initializePageRanks();
+
+ for (let i = 0; i < MAX_ITERATIONS; i++) {
+ if (this.pageRankIteration()) {
+ break;
+ }
+ }
}
/**
* @param {Doc} doc - doc for which keys are returned
- *
+ *
* This method returns a list of a document doc's keys.
*/
static documentKeys(doc: Doc) {
@@ -244,7 +349,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
const query = StrCast(this._searchString);
Doc.SetSearchQuery(query);
- this._results = [];
+ this._results.clear();
if (query) {
this.searchCollection(query);
@@ -256,16 +361,16 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
* brushes and highlights. All search matches are cleared as well.
*/
resetSearch = action(() => {
- this._results.forEach(result => {
- Doc.UnBrushDoc(result[0]);
- Doc.UnHighlightDoc(result[0]);
+ this._results.forEach((_, doc) => {
+ Doc.UnBrushDoc(doc);
+ Doc.UnHighlightDoc(doc);
Doc.ClearSearchMatches();
});
});
/**
* @param {Doc} doc - doc to be selected
- *
+ *
* This method selects a doc by either jumping to it (centering/zooming in on it)
* or opening it in a new tab.
*/
@@ -292,8 +397,11 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
const isLinkSearch: boolean = this.props.linkSearch;
+ const sortedResults = Array.from(this._results.entries()).sort((a, b) => (this._pageRanks.get(b[0]) ?? 0) - (this._pageRanks.get(a[0]) ?? 0)); // sorted by page rank
+
+ const resultsJSX = Array();
- const results = this._results.map(result => {
+ sortedResults.forEach((result) => {
var className = "searchBox-results-scroll-view-result";
if (this._selectedResult === result[0]) {
@@ -305,7 +413,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
if (this._docTypeString === "all" || this._docTypeString === result[0].type) {
validResults++;
- return (
+ resultsJSX.push(
<Tooltip key={result[0][Id]} placement={"right"} title={<><div className="dash-tooltip">{title}</div></>}>
<div onClick={isLinkSearch ?
() => this.makeLink(result[0]) :
@@ -326,12 +434,8 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
</Tooltip>
);
}
-
- return null;
});
- results.filter(result => result);
-
return (
<div style={{ pointerEvents: "all" }} className="searchBox-container">
<div className="searchBox-bar" >
@@ -345,7 +449,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps, SearchBoxDoc
{`${validResults}` + " result" + (validResults === 1 ? "" : "s")}
</div>
<div className="searchBox-results-scroll-view">
- {results}
+ {resultsJSX}
</div>
</div>
</div >
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index f16e143d8..560cf3d63 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -3,6 +3,7 @@ import { Scripting } from "../client/util/Scripting";
import { Deserializable } from "../client/util/SerializationHelper";
import { Copy, ToScriptString, ToString } from "./FieldSymbols";
import { ObjectField } from "./ObjectField";
+import { Bezier } from "bezier-js";
// Helps keep track of the current ink tool in use.
export enum InkTool {
@@ -20,6 +21,8 @@ export interface PointData {
Y: number;
}
+export type Segment = Array<Bezier>;
+
// Defines an ink as an array of points.
export type InkData = Array<PointData>;
@@ -67,6 +70,14 @@ export class InkField extends ObjectField {
this.inkData = data;
}
+ /**
+ * Extacts a simple segment from a compound Bezier curve
+ * @param segIndex the start index of the simple bezier segment to extact (eg., 0, 4, 8, ...)
+ */
+ public static Segment(inkData: InkData, segIndex: number) {
+ return new Bezier(inkData.slice(segIndex, segIndex + 4).map(pt => ({ x: pt.X, y: pt.Y })));
+ }
+
[Copy]() {
return new InkField(this.inkData);
}
diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx
index 71ddda866..88221732e 100644
--- a/src/mobile/AudioUpload.tsx
+++ b/src/mobile/AudioUpload.tsx
@@ -94,7 +94,7 @@ export class AudioUpload extends React.Component {
PanelHeight={() => 400}
renderDepth={0}
isDocumentActive={returnTrue}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
focus={emptyFunction}
layerProvider={undefined}
styleProvider={() => "rgba(0,0,0,0)"}
diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx
index 652804126..d732a6e2f 100644
--- a/src/mobile/MobileInterface.tsx
+++ b/src/mobile/MobileInterface.tsx
@@ -211,7 +211,7 @@ export class MobileInterface extends React.Component {
PanelHeight={this.returnHeight}
renderDepth={0}
isDocumentActive={returnTrue}
- isContentActive={returnFalse}
+ isContentActive={emptyFunction}
focus={DocUtils.DefaultFocus}
styleProvider={this.whitebackground}
layerProvider={undefined}