From 8bc17cecdfce184e5a426dc2332d3c9ad0406f58 Mon Sep 17 00:00:00 2001 From: vkalev Date: Sat, 10 Jul 2021 11:13:42 -0500 Subject: fixed adding point bug with breaking handle tangency --- src/client/documents/Documents.ts | 8 +- src/client/views/GestureOverlay.tsx | 2 +- src/client/views/InkControls.tsx | 18 +- src/client/views/InkHandles.tsx | 27 +-- src/client/views/InkStroke.tsx | 187 --------------------- src/client/views/InkStrokeProperties.ts | 69 ++++++-- src/client/views/InkingStroke.tsx | 187 +++++++++++++++++++++ src/client/views/PropertiesButtons.tsx | 4 +- src/client/views/collections/CollectionMenu.tsx | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 2 +- .../views/nodes/CollectionFreeFormDocumentView.tsx | 4 +- src/client/views/nodes/ColorBox.tsx | 2 +- src/client/views/nodes/DocumentContentsView.tsx | 4 +- src/client/views/nodes/DocumentView.tsx | 6 +- 14 files changed, 285 insertions(+), 237 deletions(-) delete mode 100644 src/client/views/InkStroke.tsx create mode 100644 src/client/views/InkingStroke.tsx (limited to 'src') diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 188fe31c0..24682cbd0 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -31,7 +31,7 @@ import { CollectionView, CollectionViewType } from "../views/collections/Collect import { ContextMenu } from "../views/ContextMenu"; import { ContextMenuProps } from "../views/ContextMenuItem"; import { DFLT_IMAGE_NATIVE_DIM } from "../views/globalCssVariables.scss"; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkStroke } from "../views/InkStroke"; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke } from "../views/InkingStroke"; import { AudioBox } from "../views/nodes/AudioBox"; import { ColorBox } from "../views/nodes/ColorBox"; import { ComparisonBox } from "../views/nodes/ComparisonBox"; @@ -432,7 +432,7 @@ export namespace Docs { options: { links: ComputedField.MakeFunction("links(self)") as any, hideLinkButton: true } }], [DocumentType.INK, { - layout: { view: InkStroke, dataField: defaultDataKey }, + layout: { view: InkingStroke, dataField: defaultDataKey }, options: { _fontFamily: "cursive", backgroundColor: "transparent", links: ComputedField.MakeFunction("links(self)") as any } }], [DocumentType.SCREENSHOT, { @@ -760,7 +760,7 @@ export namespace Docs { const I = new Doc(); I[Initializing] = true; I.type = DocumentType.INK; - I.layout = InkStroke.LayoutString("data"); + I.layout = InkingStroke.LayoutString("data"); I.color = color; I.fillColor = fillColor; I.strokeWidth = Number(strokeWidth); @@ -1144,7 +1144,7 @@ export namespace DocUtils { layout = AudioBox.LayoutString; } else if (field instanceof InkField) { created = Docs.Create.InkDocument(ActiveInkColor(), CurrentUserUtils.SelectedTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), (field).inkData, resolved); - layout = InkStroke.LayoutString; + layout = InkingStroke.LayoutString; } else if (field instanceof List && field[0] instanceof Doc) { created = Docs.Create.StackingDocument(DocListCast(field), resolved); layout = CollectionView.LayoutString; diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 455b3a935..491bf18b2 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -17,7 +17,7 @@ import { Scripting } from "../util/Scripting"; 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 "./InkStroke"; +import { ActiveArrowEnd, ActiveArrowStart, 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"; diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx index eeddfce4c..4d8b2c6b5 100644 --- a/src/client/views/InkControls.tsx +++ b/src/client/views/InkControls.tsx @@ -22,17 +22,17 @@ export class InkControls extends React.Component { /** * Handles the movement of a selected control point when the user clicks and drags. - * @param controlNum The index of the currently selected control point. + * @param controlIndex The index of the currently selected control point. */ @action - onControlDown = (e: React.PointerEvent, controlNum: number): void => { + onControlDown = (e: React.PointerEvent, controlIndex: number): void => { if (InkStrokeProperties.Instance) { InkStrokeProperties.Instance.moveControl(0, 0, 1); const controlUndo = UndoManager.StartBatch("DocDecs set radius"); const screenScale = this.props.ScreenToLocalTransform().Scale; setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { - InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlNum); + InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex); return false; }, () => controlUndo?.end(), emptyFunction); @@ -60,12 +60,20 @@ export class InkControls extends React.Component { } } + /** + * Updates whether a user has hovered over a particular control point or point that could be added + * on click. + */ @action onEnterControl = (i: number) => { this._overControl = i; }; @action onLeaveControl = () => { this._overControl = -1; }; @action onEnterAddPoint = (i: number) => { this._overAddPoint = i; }; @action onLeaveAddPoint = () => { this._overAddPoint = -1; }; render() { + const formatInstance = InkStrokeProperties.Instance; + if (!formatInstance) return (null); + + // Accessing the current ink's data and extracting all control points. const data = this.props.data; const controlPoints: ControlPoint[] = []; if (data.length >= 4) { @@ -87,7 +95,7 @@ export class InkControls extends React.Component { r={strokeWidth / 2} stroke={this._overAddPoint === i ? "#1F85DE" : "transparent"} strokeWidth={dotsize / 4} fill={this._overAddPoint === i ? "#1F85DE" : "transparent"} - onPointerDown={() => { InkStrokeProperties.Instance?.addPoints(pts.X, pts.Y, addedPoints, i, controlPoints); }} + onPointerDown={() => { formatInstance?.addPoints(pts.X, pts.Y, addedPoints, i, controlPoints); }} onMouseEnter={() => this.onEnterAddPoint(i)} onMouseLeave={this.onLeaveAddPoint} pointerEvents="all" @@ -103,7 +111,7 @@ export class InkControls extends React.Component { height={this._overControl === i ? strokeWidth * 1.5 : strokeWidth} width={this._overControl === i ? strokeWidth * 1.5 : strokeWidth} strokeWidth={strokeWidth / 6} stroke="#1F85DE" - fill={InkStrokeProperties.Instance?._currentPoint === control.I ? "#1F85DE" : "white"} + fill={formatInstance?._currentPoint === control.I ? "#1F85DE" : "white"} onPointerDown={(e) => { this.changeCurrPoint(control.I); this.onControlDown(e, control.I); }} onMouseEnter={() => this.onEnterControl(i)} onMouseLeave={this.onLeaveControl} diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx index 993e427b3..a33380221 100644 --- a/src/client/views/InkHandles.tsx +++ b/src/client/views/InkHandles.tsx @@ -15,33 +15,38 @@ export interface InkControlProps { @observer export class InkHandles extends React.Component { - @observable private _brokenIndices: number[] = []; - /** * Handles the movement of a selected handle point when the user clicks and drags. * @param handleNum The index of the currently selected handle point. */ - onHandleDown = (e: React.PointerEvent, handleNum: number): void => { + onHandleDown = (e: React.PointerEvent, handleIndex: number): void => { if (InkStrokeProperties.Instance) { InkStrokeProperties.Instance.moveControl(0, 0, 1); const controlUndo = UndoManager.StartBatch("DocDecs set radius"); const screenScale = this.props.ScreenToLocalTransform().Scale; - document.addEventListener("keydown", (e: KeyboardEvent) => this.onBreakTangent(e, handleNum), true); + + const order = handleIndex % 4; + const oppositeHandleIndex = order === 1 ? handleIndex - 3 : handleIndex + 3; + const controlIndex = order === 1 ? handleIndex - 1 : handleIndex + 2; + document.addEventListener("keydown", (e: KeyboardEvent) => this.onBreakTangent(e, controlIndex), true); setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { - InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleNum, this._brokenIndices); + InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex); return false; }, () => controlUndo?.end(), emptyFunction ); } } + /** + * Breaks tangent handle movement when ‘Alt’ key is held down. Adds the current handle index and + * its matching (opposite) handle to a list of broken handle indices. + * @param handleNum The index of the currently selected handle point. + */ @action - onBreakTangent = (e: KeyboardEvent, handleIndex: number) => { + onBreakTangent = (e: KeyboardEvent, controlIndex: number) => { if (["Alt"].includes(e.key)) { - const order = handleIndex % 4; - const oppositeHandleIndex = order === 1 ? handleIndex - 3 : handleIndex + 3; - if (!this._brokenIndices.includes(handleIndex) && !this._brokenIndices.includes(oppositeHandleIndex)) { - this._brokenIndices.push(handleIndex, oppositeHandleIndex); + if (!InkStrokeProperties.Instance?._brokenIndices.includes(controlIndex)) { + InkStrokeProperties.Instance?._brokenIndices.push(controlIndex); } } } @@ -49,6 +54,8 @@ export class InkHandles extends React.Component { 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.data; const handlePoints: HandlePoint[] = []; const handleLines: HandleLine[] = []; diff --git a/src/client/views/InkStroke.tsx b/src/client/views/InkStroke.tsx deleted file mode 100644 index 9ca8bdbea..000000000 --- a/src/client/views/InkStroke.tsx +++ /dev/null @@ -1,187 +0,0 @@ -import React = require("react"); -import { action, observable } from "mobx"; -import { observer } from "mobx-react"; -import { Doc } from "../../fields/Doc"; -import { documentSchema } from "../../fields/documentSchemas"; -import { InkData, InkField, InkTool, ControlPoint, HandlePoint, HandleLine } from "../../fields/InkField"; -import { makeInterface } from "../../fields/Schema"; -import { Cast, StrCast } from "../../fields/Types"; -import { TraceMobx } from "../../fields/util"; -import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils"; -import { CognitiveServices } from "../cognitive_services/CognitiveServices"; -import { InteractionUtils } from "../util/InteractionUtils"; -import { Scripting } from "../util/Scripting"; -import { UndoManager } from "../util/UndoManager"; -import { ContextMenu } from "./ContextMenu"; -import { ViewBoxBaseComponent } from "./DocComponent"; -import "./InkStroke.scss"; -import { FieldView, FieldViewProps } from "./nodes/FieldView"; -import { InkStrokeProperties } from "./InkStrokeProperties"; -import { CurrentUserUtils } from "../util/CurrentUserUtils"; -import { InkControls } from "./InkControls"; -import { InkHandles } from "./InkHandles"; - -type InkDocument = makeInterface<[typeof documentSchema]>; -const InkDocument = makeInterface(documentSchema); - -@observer -export class InkStroke extends ViewBoxBaseComponent(InkDocument) { - static readonly MaskDim = 50000; - @observable private _properties?: InkStrokeProperties; - - constructor(props: FieldViewProps & InkDocument) { - super(props); - - this._properties = InkStrokeProperties.Instance; - } - - public static LayoutString(fieldStr: string) { - return FieldView.LayoutString(InkStroke, fieldStr); - } - - analyzeStrokes() { - const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; - CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]); - } - - @action - public static toggleMask = (inkDoc: Doc) => { - inkDoc.isInkMask = !inkDoc.isInkMask; - inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined; - inkDoc.mixBlendMode = inkDoc.isInkMask ? "hard-light" : undefined; - inkDoc.color = "#9b9b9bff"; - inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined; - } - - /** - * Handles the movement of the entire ink object when the user clicks and drags. - * @param e React Pointer Event. - */ - onPointerDown = (e: React.PointerEvent) => { - if (this.props.isSelected(true)) { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, - action((e: PointerEvent, doubleTap: boolean | undefined) => - doubleTap && this._properties && (this._properties._controlButton = true)) - ); - } - } - - render() { - TraceMobx(); - - const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; - const strokeWidth = Number(this.layoutDoc.strokeWidth); - const lineTop = Math.min(...data.map(p => p.Y)); - const lineBottom = Math.max(...data.map(p => p.Y)); - const lineLeft = Math.min(...data.map(p => p.X)); - const lineRight = Math.max(...data.map(p => p.X)); - const left = lineLeft - strokeWidth / 2; - const top = lineTop - strokeWidth / 2; - const right = lineRight + strokeWidth / 2; - const bottom = lineBottom + strokeWidth / 2; - const width = Math.max(1, right - left); - const height = Math.max(1, bottom - top); - const scaleX = width === strokeWidth ? 1 : (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth); - const scaleY = height === strokeWidth ? 1 : (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth); - const strokeColor = StrCast(this.layoutDoc.color, ""); - const dotsize = Math.max(width * scaleX, height * scaleY) / 40; - - // Visually renders the polygonal line made by the user. - const inkLine = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, - StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), - StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), - StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", - this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, - false); - const selectedLine = InteractionUtils.CreatePolyline( - data, lineLeft - strokeWidth * 3, lineTop - strokeWidth * 3, "#1F85DE", strokeWidth / 6, - strokeWidth / 6,StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), - StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), - StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", - this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, - false); - // Invisible polygonal line that enables the ink to be selected by the user. - const clickableLine = InteractionUtils.CreatePolyline(data, left, top, - this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, - strokeWidth + 15, StrCast(this.layoutDoc.strokeBezier), - StrCast(this.layoutDoc.fillColor, "none"), "none", "none", undefined, scaleX, scaleY, "", - this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted", false, true); - const addedPoints = InteractionUtils.CreatePoints(data, left, top, strokeColor, strokeWidth, strokeWidth, - StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), - StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), - StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", - this.props.isSelected() && strokeWidth <= 5, false); - - return ( - { - const cm = ContextMenu.Instance; - if (cm) { - !Doc.UserDoc().noviceMode && cm.addItem({ description: "Recognize Writing", event: this.analyzeStrokes, icon: "paint-brush" }); - cm.addItem({ description: "Toggle Mask", event: () => InkStroke.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" }); - } - }} - > - - {clickableLine} - {inkLine} - {this.props.isSelected() ? selectedLine : ""} - {this.props.isSelected() && this._properties?._controlButton ? - <> - - - : ""} - - ); - } -} - - -export function SetActiveInkWidth(width: string): void { !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); } -export function SetActiveBezierApprox(bezier: string): void { ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? "" : bezier); } -export function SetActiveInkColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeInkColor = value); } -export function 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 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 ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, "0"); } -export function ActiveInkWidth(): string { return StrCast(ActiveInkPen()?.activeInkWidth, "1"); } -export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } -Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any, fill: any, arrowStart: any, arrowEnd: any, dash: any) { - CurrentUserUtils.SelectedTool = pen ? InkTool.Highlighter : InkTool.None; - SetActiveInkWidth(width); - SetActiveInkColor(color); - SetActiveFillColor(fill); - SetActiveArrowStart(arrowStart); - SetActiveArrowEnd(arrowEnd); - SetActiveDash(dash); -}); -Scripting.addGlobal(function activateEraser(pen: any) { return CurrentUserUtils.SelectedTool = pen ? InkTool.Eraser : InkTool.None; }); -Scripting.addGlobal(function activateStamp(pen: any) { return CurrentUserUtils.SelectedTool = pen ? InkTool.Stamp : InkTool.None; }); -Scripting.addGlobal(function deactivateInk() { return CurrentUserUtils.SelectedTool = InkTool.None; }); -Scripting.addGlobal(function setInkWidth(width: any) { return SetActiveInkWidth(width); }); -Scripting.addGlobal(function setInkColor(color: any) { return SetActiveInkColor(color); }); -Scripting.addGlobal(function setFillColor(fill: any) { return SetActiveFillColor(fill); }); -Scripting.addGlobal(function setActiveArrowStart(arrowStart: any) { return SetActiveArrowStart(arrowStart); }); -Scripting.addGlobal(function setActiveArrowEnd(arrowEnd: any) { return SetActiveArrowStart(arrowEnd); }); -Scripting.addGlobal(function setActiveDash(dash: any) { return SetActiveDash(dash); }); diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 812e8ff6e..4ec03c560 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -1,7 +1,7 @@ import { action, computed, observable } from "mobx"; import { Doc, Field, Opt } from "../../fields/Doc"; import { Document } from "../../fields/documentSchemas"; -import { InkField, InkData, PointData } from "../../fields/InkField"; +import { InkField, InkData, PointData, ControlPoint } from "../../fields/InkField"; import { Cast, NumCast } from "../../fields/Types"; import { DocumentType } from "../documents/DocumentTypes"; import { SelectionManager } from "../util/SelectionManager"; @@ -13,6 +13,7 @@ export class InkStrokeProperties { @observable _lock = false; @observable _controlButton = false; @observable _currentPoint = -1; + @observable _brokenIndices: number[] = []; constructor() { InkStrokeProperties.Instance = this; @@ -73,6 +74,7 @@ export class InkStrokeProperties { addPoints = (x: number, y: number, pts: { X: number, Y: number }[], index: number, control: { X: number, Y: number }[]) => { this.selectedInk?.forEach(action(inkView => { if (this.selectedInk?.length === 1) { + const newPoint = { X: x, Y: y }; const doc = Document(inkView.rootDoc); if (doc.type === DocumentType.INK) { const ink = Cast(doc.data, InkField)?.inkData; @@ -88,12 +90,25 @@ export class InkStrokeProperties { for (var i = 0; i < spNum; i++) { ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y }); } - for (var j = 0; j < 4; j++) { - newPoints.push({ X: x, Y: y }); - } + // Updating the indices of the control points whose handle tangency has been broken. + this._brokenIndices = this._brokenIndices.map((control) => { + if (control >= spNum) { + return control + 4; + } else { + return control; + } + }); + + // const [handleA, handleB] = this.getNewHandlePoints(newPoint, pts[index-1], pts[index+1]); + newPoints.push(newPoint); + newPoints.push(newPoint); + newPoints.push(newPoint); + newPoints.push(newPoint); + for (var i = spNum; i < ink.length; i++) { newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } this._currentPoint = -1; Doc.GetProto(doc).data = new InkField(newPoints); @@ -103,6 +118,20 @@ export class InkStrokeProperties { })); } + getNewHandlePoints = (newControl: PointData, a: PointData, b: PointData) => { + // find midpoint between the left and right control point of new control + // rotate midpoint by +-pi/2 to get new handle points + // multiplying x-y coordinates of both by 10/L where L is its current magnitude + const angle = this.angleChange(a, b, newControl); + const midpoint = this.rotatePoint(a, newControl, angle/2); + // const handleA = this.rotatePoint(midpoint, newControl, -Math.PI/2); + // const handleB = this.rotatePoint(midpoint, newControl, -Math.PI/2); + const handleA = { X: midpoint.X + (20 * Math.cos(-Math.PI/2)), Y: midpoint.Y + (20 * Math.sin(-Math.PI/2)) }; + const handleB = { X: midpoint.X + (20 * Math.cos(Math.PI/2)), Y: midpoint.Y + (20 * Math.sin(Math.PI/2)) }; + + return [handleA, handleB]; + } + /** * Deletes the points of the current ink instance. * @returns The changed x- and y-coordinates of the control points. @@ -195,23 +224,27 @@ export class InkStrokeProperties { return target; } + 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); + // Normalizing the vectors. + vectorA = { X: vectorA.X / magnitudeA, Y: vectorA.Y / magnitudeA }; + vectorB = { X: vectorB.X / magnitudeB, Y: vectorB.Y / magnitudeB }; + const dotProduct = vectorB.X * vectorA.X + vectorB.Y * vectorA.Y; + return Math.acos(dotProduct); + } + /** * Finds the angle difference (in radians) between two vectors relative to an arbitrary origin. */ angleChange = (a: PointData, b: PointData, origin: PointData) => { // Finding vector representation of inputted points relative to new origin. - let vectorA = { X: a.X - origin.X, Y: a.Y - origin.Y }; - let vectorB = { X: b.X - origin.X, Y: b.Y - origin.Y }; + 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 magnitudeA = Math.sqrt(vectorA.X * vectorA.X + vectorA.Y * vectorA.Y); - const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y); - // Normalizing the vectors. - vectorA = { X: vectorA.X / magnitudeA, Y: vectorA.Y / magnitudeA }; - vectorB = { X: vectorB.X / magnitudeB, Y: vectorB.Y / magnitudeB }; - const dotProduct = vectorB.X * vectorA.X + vectorB.Y * vectorA.Y; - const theta = Math.acos(dotProduct); + const theta = this.angleBetweenTwoVectors(vectorA, vectorB); return sign * theta; } @@ -220,20 +253,20 @@ export class InkStrokeProperties { */ @undoBatch @action - moveHandle = (deltaX: number, deltaY: number, handleIndex: number, brokenIndices: number[]) => + moveHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) => this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { const order = handleIndex % 4; const oldHandlePoint = ink[handleIndex]; + let oppositeHandlePoint = ink[oppositeHandleIndex]; + const controlPoint = ink[controlIndex]; const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale }; ink[handleIndex] = newHandlePoint; // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle). - if (!brokenIndices.includes(handleIndex) && handleIndex !== 1 && handleIndex !== ink.length - 2) { - let oppositeHandlePoint = order === 1 ? ink[handleIndex - 3] : ink[handleIndex + 3]; - const controlPoint = order === 1 ? ink[handleIndex - 1] : ink[handleIndex + 1]; + if (!this._brokenIndices.includes(controlIndex) && handleIndex !== 1 && handleIndex !== ink.length - 2) { const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); - order === 1 ? ink[handleIndex - 3] = oppositeHandlePoint : ink[handleIndex + 3] = oppositeHandlePoint; + ink[oppositeHandleIndex] = oppositeHandlePoint; } return ink; diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx new file mode 100644 index 000000000..b630c20cc --- /dev/null +++ b/src/client/views/InkingStroke.tsx @@ -0,0 +1,187 @@ +import React = require("react"); +import { action, observable } from "mobx"; +import { observer } from "mobx-react"; +import { Doc } from "../../fields/Doc"; +import { documentSchema } from "../../fields/documentSchemas"; +import { InkData, InkField, InkTool } from "../../fields/InkField"; +import { makeInterface } from "../../fields/Schema"; +import { Cast, StrCast } from "../../fields/Types"; +import { TraceMobx } from "../../fields/util"; +import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils"; +import { CognitiveServices } from "../cognitive_services/CognitiveServices"; +import { InteractionUtils } from "../util/InteractionUtils"; +import { Scripting } from "../util/Scripting"; +import { ContextMenu } from "./ContextMenu"; +import { ViewBoxBaseComponent } from "./DocComponent"; +import "./InkStroke.scss"; +import { FieldView, FieldViewProps } from "./nodes/FieldView"; +import { InkStrokeProperties } from "./InkStrokeProperties"; +import { CurrentUserUtils } from "../util/CurrentUserUtils"; +import { InkControls } from "./InkControls"; +import { InkHandles } from "./InkHandles"; + +type InkDocument = makeInterface<[typeof documentSchema]>; +const InkDocument = makeInterface(documentSchema); + +@observer +export class InkingStroke extends ViewBoxBaseComponent(InkDocument) { + static readonly MaskDim = 50000; + @observable private _properties?: InkStrokeProperties; + + constructor(props: FieldViewProps & InkDocument) { + super(props); + + this._properties = InkStrokeProperties.Instance; + } + + public static LayoutString(fieldStr: string) { + return FieldView.LayoutString(InkingStroke, fieldStr); + } + + analyzeStrokes() { + const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; + CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]); + } + + @action + public static toggleMask = (inkDoc: Doc) => { + inkDoc.isInkMask = !inkDoc.isInkMask; + inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined; + inkDoc.mixBlendMode = inkDoc.isInkMask ? "hard-light" : undefined; + inkDoc.color = "#9b9b9bff"; + inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined; + } + + /** + * Handles the movement of the entire ink object when the user clicks and drags. + */ + onPointerDown = (e: React.PointerEvent) => { + if (this.props.isSelected(true)) { + setupMoveUpEvents(this, e, returnFalse, emptyFunction, + action((e: PointerEvent, doubleTap: boolean | undefined) => + doubleTap && this._properties && (this._properties._controlButton = true)) + ); + } + } + + render() { + TraceMobx(); + // Extracting the ink data and formatting information of the current ink stroke. + const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; + const strokeWidth = Number(this.layoutDoc.strokeWidth); + const lineTop = Math.min(...data.map(p => p.Y)); + const lineBottom = Math.max(...data.map(p => p.Y)); + const lineLeft = Math.min(...data.map(p => p.X)); + const lineRight = Math.max(...data.map(p => p.X)); + const left = lineLeft - strokeWidth / 2; + const top = lineTop - strokeWidth / 2; + const right = lineRight + strokeWidth / 2; + const bottom = lineBottom + strokeWidth / 2; + const width = Math.max(1, right - left); + const height = Math.max(1, bottom - top); + const scaleX = width === strokeWidth ? 1 : (this.props.PanelWidth() - strokeWidth) / (width - strokeWidth); + const scaleY = height === strokeWidth ? 1 : (this.props.PanelHeight() - strokeWidth) / (height - strokeWidth); + const strokeColor = StrCast(this.layoutDoc.color, ""); + const dotsize = Math.max(width * scaleX, height * scaleY) / 40; + + // Visually renders the polygonal line made by the user. + const inkLine = InteractionUtils.CreatePolyline(data, left, top, strokeColor, strokeWidth, strokeWidth, + StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), + StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), + StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", + this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, + false); + // Thin blue line indicating that the current ink stroke is selected. + const selectedLine = InteractionUtils.CreatePolyline( + data, lineLeft - strokeWidth * 3, lineTop - strokeWidth * 3, "#1F85DE", strokeWidth / 6, + strokeWidth / 6,StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), + StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), + StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", + this.props.isSelected() && strokeWidth <= 5 && lineBottom - lineTop > 1 && lineRight - lineLeft > 1, + false); + // Invisible polygonal line that enables the ink to be selected by the user. + const clickableLine = InteractionUtils.CreatePolyline(data, left, top, + this.props.isSelected() && strokeWidth > 5 ? strokeColor : "transparent", strokeWidth, + strokeWidth + 15, StrCast(this.layoutDoc.strokeBezier), + StrCast(this.layoutDoc.fillColor, "none"), "none", "none", undefined, scaleX, scaleY, "", + this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted", false, true); + // Set of points rendered upon the ink that can be added if a user clicks on one. + const addedPoints = InteractionUtils.CreatePoints(data, left, top, strokeColor, strokeWidth, strokeWidth, + StrCast(this.layoutDoc.strokeBezier), StrCast(this.layoutDoc.fillColor, "none"), + StrCast(this.layoutDoc.strokeStartMarker), StrCast(this.layoutDoc.strokeEndMarker), + StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", + this.props.isSelected() && strokeWidth <= 5, false); + + return ( + { + const cm = ContextMenu.Instance; + if (cm) { + !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" }); + } + }} + > + + {clickableLine} + {inkLine} + {this.props.isSelected() ? selectedLine : ""} + {this.props.isSelected() && this._properties?._controlButton ? + <> + + + : ""} + + ); + } +} + + +export function SetActiveInkWidth(width: string): void { !isNaN(parseInt(width)) && ActiveInkPen() && (ActiveInkPen().activeInkWidth = width); } +export function SetActiveBezierApprox(bezier: string): void { ActiveInkPen() && (ActiveInkPen().activeInkBezier = isNaN(parseInt(bezier)) ? "" : bezier); } +export function SetActiveInkColor(value: string) { ActiveInkPen() && (ActiveInkPen().activeInkColor = value); } +export function 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 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 ActiveDash(): string { return StrCast(ActiveInkPen()?.activeDash, "0"); } +export function ActiveInkWidth(): string { return StrCast(ActiveInkPen()?.activeInkWidth, "1"); } +export function ActiveInkBezierApprox(): string { return StrCast(ActiveInkPen()?.activeInkBezier); } +Scripting.addGlobal(function activateBrush(pen: any, width: any, color: any, fill: any, arrowStart: any, arrowEnd: any, dash: any) { + CurrentUserUtils.SelectedTool = pen ? InkTool.Highlighter : InkTool.None; + SetActiveInkWidth(width); + SetActiveInkColor(color); + SetActiveFillColor(fill); + SetActiveArrowStart(arrowStart); + SetActiveArrowEnd(arrowEnd); + SetActiveDash(dash); +}); +Scripting.addGlobal(function activateEraser(pen: any) { return CurrentUserUtils.SelectedTool = pen ? InkTool.Eraser : InkTool.None; }); +Scripting.addGlobal(function activateStamp(pen: any) { return CurrentUserUtils.SelectedTool = pen ? InkTool.Stamp : InkTool.None; }); +Scripting.addGlobal(function deactivateInk() { return CurrentUserUtils.SelectedTool = InkTool.None; }); +Scripting.addGlobal(function setInkWidth(width: any) { return SetActiveInkWidth(width); }); +Scripting.addGlobal(function setInkColor(color: any) { return SetActiveInkColor(color); }); +Scripting.addGlobal(function setFillColor(fill: any) { return SetActiveFillColor(fill); }); +Scripting.addGlobal(function setActiveArrowStart(arrowStart: any) { return SetActiveArrowStart(arrowStart); }); +Scripting.addGlobal(function setActiveArrowEnd(arrowEnd: any) { return SetActiveArrowStart(arrowEnd); }); +Scripting.addGlobal(function setActiveDash(dash: any) { return SetActiveDash(dash); }); diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index 920244463..5c41a96d0 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -11,7 +11,7 @@ import { DocumentType } from '../documents/DocumentTypes'; import { SelectionManager } from '../util/SelectionManager'; import { undoBatch } from '../util/UndoManager'; import { CollectionViewType } from './collections/CollectionView'; -import { InkStroke } from './InkStroke'; +import { InkingStroke } from './InkingStroke'; import { DocumentView } from './nodes/DocumentView'; import './PropertiesButtons.scss'; import React = require("react"); @@ -56,7 +56,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { return this.propertyToggleBtn("Dictate", "_showAudio", on => `${on ? "Hide" : "Show"} dictation/recording controls`, on => "microphone"); } @computed get maskButton() { - return this.propertyToggleBtn("Mask", "isInkMask", on => on ? "Make plain ink" : "Make highlight mask", on => "paint-brush", (dv, doc) => InkStroke.toggleMask(dv?.layoutDoc || doc)); + return this.propertyToggleBtn("Mask", "isInkMask", on => on ? "Make plain ink" : "Make highlight mask", on => "paint-brush", (dv, doc) => InkingStroke.toggleMask(dv?.layoutDoc || doc)); } @computed get clustersButton() { return this.propertyToggleBtn("Clusters", "_useClusters", on => `${on ? "Hide" : "Show"} clusters`, on => "braille"); diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 65061d260..6e6fabd0d 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -25,7 +25,7 @@ import { undoBatch } from "../../util/UndoManager"; import { AntimodeMenu, AntimodeMenuProps } from "../AntimodeMenu"; import { EditableView } from "../EditableView"; import { GestureOverlay } from "../GestureOverlay"; -import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, ActiveArrowStart, ActiveArrowEnd } from "../InkStroke"; +import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, ActiveArrowStart, ActiveArrowEnd } from "../InkingStroke"; import { CollectionFreeFormDocumentView } from "../nodes/CollectionFreeFormDocumentView"; import { DocumentView } from "../nodes/DocumentView"; import { RichTextMenu } from "../nodes/formattedText/RichTextMenu"; diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 9e81a6cda..accb80c5a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -32,7 +32,7 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/globalCssVariables.scss" import { Timeline } from "../../animationtimeline/Timeline"; import { ContextMenu } from "../../ContextMenu"; import { DocumentDecorations } from "../../DocumentDecorations"; -import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkStroke"; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkingStroke"; import { LightboxView } from "../../LightboxView"; import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView"; import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView"; diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index 744a9c5de..092823603 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -13,7 +13,7 @@ import { SelectionManager } from "../../util/SelectionManager"; import { Transform } from "../../util/Transform"; import { CollectionFreeFormView } from "../collections/collectionFreeForm/CollectionFreeFormView"; import { DocComponent } from "../DocComponent"; -import { InkStroke } from "../InkStroke"; +import { InkingStroke } from "../InkingStroke"; import { StyleProp } from "../StyleProvider"; import "./CollectionFreeFormDocumentView.scss"; import { DocumentView, DocumentViewProps } from "./DocumentView"; @@ -38,7 +38,7 @@ export class CollectionFreeFormDocumentView extends DocComponent { render() { TraceMobx(); - const xshift = () => (this.props.Document.isInkMask ? InkStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined); - const yshift = () => (this.props.Document.isInkMask ? InkStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined); + const xshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Xshift) <= 0.001 ? this.props.PanelWidth() : undefined); + const yshift = () => (this.props.Document.isInkMask ? InkingStroke.MaskDim : Math.abs(this.Yshift) <= 0.001 ? this.props.PanelHeight() : undefined); return (
{!this.props.Document || !this.props.PanelWidth() ? (null) : (