From 441a3dab4ada425d28a55435be51339e3d28c892 Mon Sep 17 00:00:00 2001 From: vkalev <50213748+vkalev@users.noreply.github.com> Date: Mon, 21 Jun 2021 17:44:19 -0500 Subject: adding comments --- src/client/views/InkStrokeProperties.ts | 43 +++++++++++++++++++++++++++++++++ src/client/views/InkingStroke.tsx | 26 +++++++++++++++++--- src/fields/InkField.ts | 4 +++ 3 files changed, 70 insertions(+), 3 deletions(-) (limited to 'src') diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index b13b04f68..533fdf006 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -17,8 +17,11 @@ export class InkStrokeProperties { private _lastDash = "2"; private _inkDocs: { x: number, y: number, width: number, height: number }[] = []; + // Indicates whether the ink is locked. @observable _lock = false; + // Indicates whether the ink's format is being currently edited (displaying of control points). @observable _controlBtn = false; + // Stores the index of the current selected control point of the ink instance. @observable _currPoint = -1; getField(key: string) { @@ -80,6 +83,14 @@ export class InkStrokeProperties { InkStrokeProperties.Instance = this; } + /** + * Adds a new control point to the ink instance when editing its format. + * @param x The x-coordinate of the current new point. + * @param y The y-coordinate of the current new point. + * @param pts The list containing all of the points to be added in PointData form. + * @param index The index of the current new point. + * @param control The list of all control points of the ink. + */ @undoBatch @action addPoints = (x: number, y: number, pts: { X: number, Y: number }[], index: number, control: { X: number, Y: number }[]) => { @@ -115,6 +126,12 @@ export class InkStrokeProperties { })); } + /** + * Helper function that enables other functions to be applied to a particular ink instance. + * @param func The inputted function. + * @param requireCurrPoint Indicates whether the current selected point is needed. + * @returns The applied function. + */ applyFunction = (func: (doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { X: number, Y: number }[] | undefined, requireCurrPoint: boolean = false) => { var appliedFunc = false; this.selectedInk?.forEach(action(inkView => { @@ -145,6 +162,10 @@ export class InkStrokeProperties { return appliedFunc; } + /** + * Deletes the points of the current ink instance. + * @returns The changed x- and y-coordinates of the control points. + */ @undoBatch @action deletePoints = () => this.applyFunction((doc: Doc, ink: InkData) => { @@ -168,6 +189,11 @@ export class InkStrokeProperties { return newPoints; }, true); + /** + * Rotates the points of the current ink instance by a certain angle degree. + * @param angle The angle at which to rotate the ink (all of its x- and y-coordinates). + * @returns The changed x- and y-coordinates of the control points. + */ @undoBatch @action rotate = (angle: number) => { @@ -186,6 +212,13 @@ export class InkStrokeProperties { }); } + /** + * Handles the movement / scaling of control points of an ink instance. + * @param xDiff The movement of the control point's x-coordinate. + * @param yDiff The movement of the control point's y-coordinate. + * @param controlNum The index of the current control point selected. + * @returns The changed x- and y-coordinates of the control points. + */ @undoBatch @action control = (xDiff: number, yDiff: number, controlNum: number) => @@ -209,6 +242,11 @@ export class InkStrokeProperties { return newPoints; }); + /** + * Changes the color of the border of the ink instance. + * @param color The new hex value to change the border to. + * @returns true. + */ @undoBatch @action switchStk = (color: ColorState) => { @@ -217,6 +255,11 @@ export class InkStrokeProperties { return true; } + /** + * Changes the color of the fill of the ink instance. + * @param color The new hex value to change the fill to. + * @returns true. + */ @undoBatch @action switchFil = (color: ColorState) => { diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 449019ca8..859e53b97 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -41,6 +41,11 @@ export class InkingStroke extends ViewBoxBaseComponent { if (InkStrokeProperties.Instance) { @@ -56,6 +61,10 @@ export class InkingStroke extends ViewBoxBaseComponent { if (InkStrokeProperties.Instance) { @@ -64,6 +73,10 @@ export class InkingStroke extends ViewBoxBaseComponent { if (["-", "Backspace", "Delete"].includes(e.key)) { @@ -71,6 +84,10 @@ export class InkingStroke extends ViewBoxBaseComponent { if (this.props.isSelected(true)) { setupMoveUpEvents(this, e, returnFalse, emptyFunction, action((e: PointerEvent, doubleTap: boolean | undefined) => @@ -102,17 +119,18 @@ export class InkingStroke extends ViewBoxBaseComponent 1 && lineRgt - lineLft > 1, false); + // Invisible polygonal line that enables the ink to be selected by the user. const hpoints = 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); - //points for adding const apoints = 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), @@ -153,24 +171,27 @@ export class InkingStroke extends ViewBoxBaseComponent { formatInstance.addPoints(pts.X, pts.Y, apoints, i, controlPoints); }} pointerEvents="all" cursor="all-scroll" /> ); + // Green circles that allow the user to edit the curvature of the line using the selected point as the anchor. const handles = handlePoints.map((pts, i) => this.onControlDown(e, pts.I)} pointerEvents="all" cursor="default" display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> ); - + // Points (red circles) of the ink that are made visible to user when editing its format. const controls = controlPoints.map((pts, i) => { this.changeCurrPoint(pts.I); this.onControlDown(e, pts.I); }} pointerEvents="all" cursor="default" /> ); + // Set of two green lines (each with a handle at the end) that are rendered perpendicular to the current selected point while editing. const handleLines = handleLine.map((pts, i) => ); - return ( ; const pointSchema = createSimpleSchema({ @@ -28,6 +31,7 @@ const strokeDataSchema = createSimpleSchema({ "*": true }); +// Holistic class representing the store of an ink. @Deserializable("ink") export class InkField extends ObjectField { @serializable(list(object(strokeDataSchema))) -- cgit v1.2.3-70-g09d2 From b0efa4a390415072eaeb06c8719ea57d73e10466 Mon Sep 17 00:00:00 2001 From: vkalev <50213748+vkalev@users.noreply.github.com> Date: Wed, 30 Jun 2021 12:52:52 -0500 Subject: ink Bézier handle movement fixed + small visual changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/client/util/InteractionUtils.tsx | 2 +- src/client/views/InkStrokeProperties.ts | 119 ++++++++++++++++++++++++-------- src/client/views/InkingStroke.scss | 18 ++--- src/client/views/InkingStroke.tsx | 93 ++++++++++++++----------- src/fields/InkField.ts | 24 +++++++ 5 files changed, 177 insertions(+), 79 deletions(-) (limited to 'src') diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 01d00db30..ba935e3bf 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -208,7 +208,7 @@ export namespace InteractionUtils { (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt); @@ -79,10 +78,6 @@ export class InkStrokeProperties { }); } - constructor() { - InkStrokeProperties.Instance = this; - } - /** * Adds a new control point to the ink instance when editing its format. * @param x The x-coordinate of the current new point. @@ -213,35 +208,99 @@ export class InkStrokeProperties { } /** - * Handles the movement / scaling of control points of an ink instance. - * @param xDiff The movement of the control point's x-coordinate. - * @param yDiff The movement of the control point's y-coordinate. - * @param controlNum The index of the current control point selected. - * @returns The changed x- and y-coordinates of the control points. + * Handles the movement/scaling of a control point. */ @undoBatch @action - control = (xDiff: number, yDiff: number, controlNum: number) => - this.applyFunction((doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { + moveControl = (deltaX: number, deltaY: number, controlIndex: number) => + this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { const newPoints: { X: number, Y: number }[] = []; - const order = controlNum % 4; + const order = controlIndex % 4; for (var i = 0; i < ink.length; i++) { - newPoints.push( - (controlNum === i || - (order === 0 && i === controlNum + 1) || - (order === 0 && controlNum !== 0 && i === controlNum - 2) || - (order === 0 && controlNum !== 0 && i === controlNum - 1) || - (order === 3 && i === controlNum - 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 1) || - (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 2) || - ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlNum === 0 || controlNum === ink.length - 1)) - ) ? - { X: ink[i].X - xDiff / ptsXscale, Y: ink[i].Y - yDiff / ptsYscale } : - { X: ink[i].X, Y: ink[i].Y }); + const leftHandlePoint = order === 0 && i === controlIndex + 1; + const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2; + if (controlIndex === i || + leftHandlePoint || + rightHandlePoint || + (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || + (order === 3 && i === controlIndex - 1) || + (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) || + (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) || + ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))) { + newPoints.push({ X: ink[i].X - deltaX / xScale, Y: ink[i].Y - deltaY / yScale }); + } else { + newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } } return newPoints; }); + /** + * Rotates the target point about the origin point for a given angle (radians). + */ + @action + rotatePoint = (target: PointData, origin: PointData, angle: number) => { + target.X -= origin.X; + target.Y -= origin.Y; + const newX = Math.cos(angle) * target.X - Math.sin(angle) * target.Y; + const newY = Math.sin(angle) * target.X + Math.cos(angle) * target.Y; + target.X = newX + origin.X; + target.Y = newY + origin.Y; + return target + } + + /** + * 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 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); + return sign * theta; + } + + /** + * Handles the movement/scaling of a handle point. + */ + @undoBatch + @action + moveHandle = (deltaX: number, deltaY: number, handleIndex: number) => + this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { + const newPoints: { X: number, Y: number }[] = []; + const order = handleIndex % 4; + let newHandlePoint = { X: 0, Y: 0 }; + + for (var i = 0; i < ink.length; i++) { + if (handleIndex === i) { + newHandlePoint = { X: ink[i].X - deltaX / xScale, Y: ink[i].Y - deltaY / yScale }; + newPoints.push({ X: newHandlePoint.X, Y: newHandlePoint.Y }); + } else { + newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } + } + + if (handleIndex !== 1 && handleIndex !== ink.length - 2) { + const oldHandlePoint = ink[handleIndex]; + let oppositeHandlePoint = order === 1 ? ink[handleIndex - 3] : ink[handleIndex + 3]; + const controlPoint = order === 1 ? ink[handleIndex - 1] : ink[handleIndex + 1]; + const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); + oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); + order === 1 ? newPoints[handleIndex - 3] = oppositeHandlePoint : newPoints[handleIndex + 3] = oppositeHandlePoint; + } + + return newPoints; + }); + /** * Changes the color of the border of the ink instance. * @param color The new hex value to change the border to. diff --git a/src/client/views/InkingStroke.scss b/src/client/views/InkingStroke.scss index 30ab1967e..f67b1779d 100644 --- a/src/client/views/InkingStroke.scss +++ b/src/client/views/InkingStroke.scss @@ -1,11 +1,11 @@ .inkingStroke { - mix-blend-mode: multiply; - stroke-linejoin: round; - stroke-linecap: round; - overflow: visible !important; - transform-origin: top left; + mix-blend-mode: multiply; + stroke-linejoin: round; + stroke-linecap: round; + overflow: visible !important; + transform-origin: top left; - svg:not(:root) { - overflow: visible !important; - } -} \ No newline at end of file + svg:not(:root) { + overflow: visible !important; + } +} diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 859e53b97..163eb05b0 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -2,7 +2,7 @@ import { action } 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 { 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"; @@ -28,33 +28,51 @@ export class InkingStroke extends ViewBoxBaseComponent { + analyzeStrokes = () => { const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]); } - public static toggleMask = action((inkDoc: Doc) => { + @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 a selected control point when the user clicks and drags. - * @param e React Pointer Event. - * @param controlNum The number of the currently selected control point. + * @param controlNum The index of the currently selected control point. */ @action onControlDown = (e: React.PointerEvent, controlNum: number): void => { if (InkStrokeProperties.Instance) { - InkStrokeProperties.Instance.control(0, 0, 1); + 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); + return false; + }, + () => controlUndo?.end(), emptyFunction); + } + } + + /** + * Handles the movement of a selected handle point when the user clicks and drags. + * @param controlNum The index of the currently selected handle point. + */ + onHandleDown = (e: React.PointerEvent, handleNum: 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?.control(-delta[0] * screenScale, -delta[1] * screenScale, controlNum); + InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleNum); return false; }, () => controlUndo?.end(), emptyFunction); @@ -69,7 +87,7 @@ export class InkingStroke extends ViewBoxBaseComponent { if (InkStrokeProperties.Instance) { InkStrokeProperties.Instance._currPoint = i; - document.addEventListener("keydown", this.delPts, true); + document.addEventListener("keydown", this.onDelete, true); } } @@ -78,7 +96,7 @@ export class InkingStroke extends ViewBoxBaseComponent { + onDelete = (e: KeyboardEvent) => { if (["-", "Backspace", "Delete"].includes(e.key)) { if (InkStrokeProperties.Instance?.deletePoints()) e.stopPropagation(); } @@ -101,7 +119,6 @@ export class InkingStroke extends ViewBoxBaseComponent p.X); const ys = data.map(p => p.Y); @@ -118,12 +135,18 @@ export class InkingStroke extends ViewBoxBaseComponent 1 && lineRgt - lineLft > 1, false); + + const selectedLine = InteractionUtils.CreatePolyline(data, lineLft - 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 && lineBot - lineTop > 1 && lineRgt - lineLft > 1, false); // Invisible polygonal line that enables the ink to be selected by the user. const hpoints = InteractionUtils.CreatePolyline(data, left, top, @@ -136,10 +159,13 @@ export class InkingStroke extends ViewBoxBaseComponent= 4) { + + // create separate functions for these for (var i = 0; i <= data.length - 4; i += 4) { controlPoints.push({ X: data[i].X, Y: data[i].Y, I: i }); controlPoints.push({ X: data[i + 3].X, Y: data[i + 3].Y, I: i + 3 }); @@ -147,29 +173,21 @@ export class InkingStroke extends ViewBoxBaseComponent @@ -178,27 +196,24 @@ export class InkingStroke extends ViewBoxBaseComponent { formatInstance.addPoints(pts.X, pts.Y, apoints, i, controlPoints); }} pointerEvents="all" cursor="all-scroll" /> ); - // Green circles that allow the user to edit the curvature of the line using the selected point as the anchor. + // Blue circles that allow the user to edit the curvature of the line using the selected control point as the anchor. const handles = handlePoints.map((pts, i) => - this.onControlDown(e, pts.I)} pointerEvents="all" cursor="default" display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> + this.onHandleDown(e, pts.I)} pointerEvents="all" cursor="default" display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> ); - // Points (red circles) of the ink that are made visible to user when editing its format. + // Control points of the ink (blue outlined squares) that are made visible to user when editing its format. const controls = controlPoints.map((pts, i) => - { this.changeCurrPoint(pts.I); this.onControlDown(e, pts.I); }} pointerEvents="all" cursor="default" /> ); - // Set of two green lines (each with a handle at the end) that are rendered perpendicular to the current selected point while editing. + // Set of two blue lines (each with a handle at the end) that are rendered perpendicular to the current selected point while editing. const handleLines = handleLine.map((pts, i) => - + - ); @@ -227,8 +242,8 @@ export class InkingStroke extends ViewBoxBaseComponent ); } diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index b79a03146..c158dac42 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -13,6 +13,7 @@ export enum InkTool { Stamp = "stamp" } + // Defines a point in an ink as a pair of x- and y-coordinates. export interface PointData { X: number; @@ -22,6 +23,29 @@ export interface PointData { // Defines an ink as an array of points. export type InkData = Array; +export interface ControlPoint { + X: number; + Y: number; + I: number; +} + +export interface HandlePoint { + X: number; + Y: number; + I: number; + dot1: number; + dot2: number; +} + +export interface HandleLine { + X1: number; + Y1: number; + X2: number; + Y2: number; + dot1: number; + dot2: number; +} + const pointSchema = createSimpleSchema({ X: true, Y: true }); -- cgit v1.2.3-70-g09d2 From d9b5d949c22658531504c95cc94e30c824519cd1 Mon Sep 17 00:00:00 2001 From: vkalev <50213748+vkalev@users.noreply.github.com> Date: Thu, 1 Jul 2021 12:44:52 -0500 Subject: ink controls resize on hover --- src/client/views/DocumentDecorations.tsx | 2 +- src/client/views/InkControl.tsx | 103 ++++++++++++++++++++++ src/client/views/InkStrokeProperties.ts | 141 ++++++++++++++++--------------- src/client/views/InkingStroke.tsx | 35 +------- 4 files changed, 179 insertions(+), 102 deletions(-) create mode 100644 src/client/views/InkControl.tsx (limited to 'src') diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index bf939d57c..d1682c09d 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -201,7 +201,7 @@ export class DocumentDecorations extends React.Component<{ boundsLeft: number, b (e: PointerEvent, down: number[], delta: number[]) => { const movement = { X: delta[0], Y: e.clientY - down[1] }; const angle = Math.max(1, Math.abs(movement.Y / 10)); - InkStrokeProperties.Instance?.rotate(2 * movement.X / angle * (Math.PI / 180)); + InkStrokeProperties.Instance?.rotateInk(2 * movement.X / angle * (Math.PI / 180)); return false; }, () => this._rotateUndo?.end(), diff --git a/src/client/views/InkControl.tsx b/src/client/views/InkControl.tsx new file mode 100644 index 000000000..ae0fc0b22 --- /dev/null +++ b/src/client/views/InkControl.tsx @@ -0,0 +1,103 @@ +import React = require("react"); +import { observable, action } from "mobx"; +import { observer } from "mobx-react"; +import { InkStrokeProperties } from "./InkStrokeProperties"; +import { setupMoveUpEvents, emptyFunction } from "../../Utils"; +import { UndoManager } from "../util/UndoManager"; +import { ControlPoint } from "../../fields/InkField"; +import { Transform } from "../util/Transform"; + +export interface InkControlProps { + control: ControlPoint; + left: number; + top: number; + scaleX: number; + scaleY: number; + strokeWidth: number; + ScreenToLocalTransform: () => Transform; +} + +@observer +export class InkControl extends React.Component { + @observable private _overControl = false; + + @action + onPointerEnter = () => { + this._overControl = true; + } + + @action + onPointerLeave = () => { + this._overControl = false; + } + + /** + * Handles the movement of a selected control point when the user clicks and drags. + * @param controlNum The index of the currently selected control point. + */ + @action + onControlDown = (e: React.PointerEvent, controlNum: 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); + return false; + }, + () => controlUndo?.end(), emptyFunction); + } + } + + /** + * Deletes the currently selected points. + * @param e Keyboard Event. + */ + @action + onDelete = (e: KeyboardEvent) => { + if (["-", "Backspace", "Delete"].includes(e.key)) { + if (InkStrokeProperties.Instance?.deletePoints()) e.stopPropagation(); + } + } + + /** + * Changes the current selected control point. + * @param i The number of the point to be selected. + */ + @action + changeCurrPoint = (i: number) => { + if (InkStrokeProperties.Instance) { + InkStrokeProperties.Instance._currPoint = i; + document.addEventListener("keydown", this.onDelete, true); + } + } + + + render() { + const control = this.props.control; + const left = this.props.left; + const top = this.props.top; + const scaleX = this.props.scaleX; + const scaleY = this.props.scaleY; + const strokeWidth = this.props.strokeWidth; + + return ( + + { this.changeCurrPoint(control.I); this.onControlDown(e, control.I); }} + onMouseEnter={this.onPointerEnter} + onMouseLeave={this.onPointerLeave} + pointerEvents="all" + cursor="default" + /> + + ) + } +} \ No newline at end of file diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 5c5395984..720a89334 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -32,51 +32,52 @@ export class InkStrokeProperties { const inks = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK); return inks.length ? inks : undefined; } - @computed get unFilled() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.fillColor ? true : false, true) || false; } - @computed get unStrokd() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.color ? true : false, true) || false; } - @computed get solidFil() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.fillColor ? true : false, true) || false; } - @computed get solidStk() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.color && (!i.rootDoc.strokeDash || i.rootDoc.strokeDash === "0") ? true : false, true) || false; } - @computed get dashdStk() { return !this.unStrokd && this.getField("strokeDash") || ""; } - @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; } - @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; } - @computed get widthStk() { return this.getField("strokeWidth") || "1"; } - @computed get markHead() { return this.getField("strokeStartMarker") || ""; } - @computed get markTail() { return this.getField("strokeEndMarker") || ""; } - @computed get shapeHgt() { return this.getField("_height"); } - @computed get shapeWid() { return this.getField("_width"); } - @computed get shapeXps() { return this.getField("x"); } - @computed get shapeYps() { return this.getField("y"); } - @computed get shapeRot() { return this.getField("rotation"); } - set unFilled(value) { this.colorFil = value ? "" : this._lastFill; } - set solidFil(value) { this.unFilled = !value; } - set colorFil(value) { value && (this._lastFill = value); this.selectedInk?.forEach(i => i.rootDoc.fillColor = value ? value : undefined); } - set colorStk(value) { value && (this._lastLine = value); this.selectedInk?.forEach(i => i.rootDoc.color = value ? value : undefined); } - set markHead(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeStartMarker = value); } - set markTail(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeEndMarker = value); } - set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } - set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } - set dashdStk(value) { - value && (this._lastDash = value) && (this.unStrokd = false); - this.selectedInk?.forEach(i => i.rootDoc.strokeDash = value ? this._lastDash : undefined); - } - set shapeXps(value) { this.selectedInk?.forEach(i => i.rootDoc.x = Number(value)); } - set shapeYps(value) { this.selectedInk?.forEach(i => i.rootDoc.y = Number(value)); } - set shapeRot(value) { this.selectedInk?.forEach(i => i.rootDoc.rotation = Number(value)); } - set widthStk(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = Number(value)); } - set shapeWid(value) { - this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - const oldWidth = NumCast(i.rootDoc._width); - i.rootDoc._width = Number(value); - this._lock && (i.rootDoc._height = (i.rootDoc._width * NumCast(i.rootDoc._height)) / oldWidth); - }); - } - set shapeHgt(value) { - this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - const oldHeight = NumCast(i.rootDoc._height); - i.rootDoc._height = Number(value); - this._lock && (i.rootDoc._width = (i.rootDoc._height * NumCast(i.rootDoc._width)) / oldHeight); - }); - } + + // @computed get unFilled() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.fillColor ? true : false, true) || false; } + // @computed get unStrokd() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.color ? true : false, true) || false; } + // @computed get solidFil() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.fillColor ? true : false, true) || false; } + // @computed get solidStk() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.color && (!i.rootDoc.strokeDash || i.rootDoc.strokeDash === "0") ? true : false, true) || false; } + // @computed get dashdStk() { return !this.unStrokd && this.getField("strokeDash") || ""; } + // @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; } + // @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; } + // @computed get widthStk() { return this.getField("strokeWidth") || "1"; } + // @computed get markHead() { return this.getField("strokeStartMarker") || ""; } + // @computed get markTail() { return this.getField("strokeEndMarker") || ""; } + // @computed get shapeHgt() { return this.getField("_height"); } + // @computed get shapeWid() { return this.getField("_width"); } + // @computed get shapeXps() { return this.getField("x"); } + // @computed get shapeYps() { return this.getField("y"); } + // @computed get shapeRot() { return this.getField("rotation"); } + // set unFilled(value) { this.colorFil = value ? "" : this._lastFill; } + // set solidFil(value) { this.unFilled = !value; } + // set colorFil(value) { value && (this._lastFill = value); this.selectedInk?.forEach(i => i.rootDoc.fillColor = value ? value : undefined); } + // set colorStk(value) { value && (this._lastLine = value); this.selectedInk?.forEach(i => i.rootDoc.color = value ? value : undefined); } + // set markHead(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeStartMarker = value); } + // set markTail(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeEndMarker = value); } + // set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } + // set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } + // set dashdStk(value) { + // value && (this._lastDash = value) && (this.unStrokd = false); + // this.selectedInk?.forEach(i => i.rootDoc.strokeDash = value ? this._lastDash : undefined); + // } + // set shapeXps(value) { this.selectedInk?.forEach(i => i.rootDoc.x = Number(value)); } + // set shapeYps(value) { this.selectedInk?.forEach(i => i.rootDoc.y = Number(value)); } + // set shapeRot(value) { this.selectedInk?.forEach(i => i.rootDoc.rotation = Number(value)); } + // set widthStk(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = Number(value)); } + // set shapeWid(value) { + // this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { + // const oldWidth = NumCast(i.rootDoc._width); + // i.rootDoc._width = Number(value); + // this._lock && (i.rootDoc._height = (i.rootDoc._width * NumCast(i.rootDoc._height)) / oldWidth); + // }); + // } + // set shapeHgt(value) { + // this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { + // const oldHeight = NumCast(i.rootDoc._height); + // i.rootDoc._height = Number(value); + // this._lock && (i.rootDoc._width = (i.rootDoc._height * NumCast(i.rootDoc._width)) / oldHeight); + // }); + // } /** * Adds a new control point to the ink instance when editing its format. @@ -191,7 +192,7 @@ export class InkStrokeProperties { */ @undoBatch @action - rotate = (angle: number) => { + rotateInk = (angle: number) => { this.applyFunction((doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X)); const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); @@ -301,29 +302,29 @@ export class InkStrokeProperties { return newPoints; }); - /** - * Changes the color of the border of the ink instance. - * @param color The new hex value to change the border to. - * @returns true. - */ - @undoBatch - @action - switchStk = (color: ColorState) => { - const val = String(color.hex); - this.colorStk = val; - return true; - } + // /** + // * Changes the color of the border of the ink instance. + // * @param color The new hex value to change the border to. + // * @returns true. + // */ + // @undoBatch + // @action + // switchStk = (color: ColorState) => { + // const val = String(color.hex); + // this.colorStk = val; + // return true; + // } - /** - * Changes the color of the fill of the ink instance. - * @param color The new hex value to change the fill to. - * @returns true. - */ - @undoBatch - @action - switchFil = (color: ColorState) => { - const val = String(color.hex); - this.colorFil = val; - return true; - } + // /** + // * Changes the color of the fill of the ink instance. + // * @param color The new hex value to change the fill to. + // * @returns true. + // */ + // @undoBatch + // @action + // switchFil = (color: ColorState) => { + // const val = String(color.hex); + // this.colorFil = val; + // return true; + // } } \ No newline at end of file diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 163eb05b0..9555557ec 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -18,6 +18,7 @@ import { FieldView, FieldViewProps } from "./nodes/FieldView"; import React = require("react"); import { InkStrokeProperties } from "./InkStrokeProperties"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; +import { InkControl } from "./InkControl"; type InkDocument = makeInterface<[typeof documentSchema]>; const InkDocument = makeInterface(documentSchema); @@ -79,29 +80,6 @@ export class InkingStroke extends ViewBoxBaseComponent { - if (InkStrokeProperties.Instance) { - InkStrokeProperties.Instance._currPoint = i; - document.addEventListener("keydown", this.onDelete, true); - } - } - - /** - * Deletes the currently selected points. - * @param e Keyboard Event. - */ - @action - onDelete = (e: KeyboardEvent) => { - if (["-", "Backspace", "Delete"].includes(e.key)) { - if (InkStrokeProperties.Instance?.deletePoints()) e.stopPropagation(); - } - } - /** * Handles the movement of the entire ink object when the user clicks and drags. * @param e React Pointer Event. @@ -113,7 +91,7 @@ export class InkingStroke extends ViewBoxBaseComponent 1 && lineRgt - lineLft > 1, false); - const selectedLine = InteractionUtils.CreatePolyline(data, lineLft - strokeWidth * 3, lineTop - strokeWidth * 3, "#1F85DE", strokeWidth / 6, strokeWidth / 6, + const selectedLine = InteractionUtils.CreatePolyline(data, lineLft - strokeWidth * 4 * scaleX, lineTop - strokeWidth * 4 * scaleX, "#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 && lineBot - lineTop > 1 && lineRgt - lineLft > 1, false); @@ -203,12 +181,7 @@ export class InkingStroke extends ViewBoxBaseComponent this.onHandleDown(e, pts.I)} pointerEvents="all" cursor="default" display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> ); // Control points of the ink (blue outlined squares) that are made visible to user when editing its format. - const controls = controlPoints.map((pts, i) => - - { this.changeCurrPoint(pts.I); this.onControlDown(e, pts.I); }} pointerEvents="all" cursor="default" - /> - ); + const controls = controlPoints.map((pts, i) => ); // Set of two blue lines (each with a handle at the end) that are rendered perpendicular to the current selected point while editing. const handleLines = handleLine.map((pts, i) => -- cgit v1.2.3-70-g09d2 From 89c8891e9def96a871d36ab7772e453b8d8c21c1 Mon Sep 17 00:00:00 2001 From: vkalev Date: Fri, 2 Jul 2021 11:58:06 -0500 Subject: indicator appears on hover for ink control points to be added --- src/client/views/InkControl.tsx | 8 +++----- src/client/views/InkingStroke.tsx | 23 ++++++++++++++++++----- 2 files changed, 21 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/client/views/InkControl.tsx b/src/client/views/InkControl.tsx index ae0fc0b22..accb16f61 100644 --- a/src/client/views/InkControl.tsx +++ b/src/client/views/InkControl.tsx @@ -51,8 +51,7 @@ export class InkControl extends React.Component { } /** - * Deletes the currently selected points. - * @param e Keyboard Event. + * Deletes the currently selected point. */ @action onDelete = (e: KeyboardEvent) => { @@ -63,7 +62,6 @@ export class InkControl extends React.Component { /** * Changes the current selected control point. - * @param i The number of the point to be selected. */ @action changeCurrPoint = (i: number) => { @@ -85,8 +83,8 @@ export class InkControl extends React.Component { return ( (InkDocument) { private _controlUndo?: UndoManager.Batch; + @observable private _overAddPoint = -1; + + static readonly MaskDim = 50000; public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); } @@ -34,6 +37,17 @@ export class InkingStroke extends ViewBoxBaseComponent { + this._overAddPoint = i; + } + + @action + onPointerLeave = () => { + this._overAddPoint = -1; + } + + @action public static toggleMask = (inkDoc: Doc) => { inkDoc.isInkMask = !inkDoc.isInkMask; @@ -91,7 +105,6 @@ export class InkingStroke extends ViewBoxBaseComponent 1 && lineRgt - lineLft > 1, false); - const selectedLine = InteractionUtils.CreatePolyline(data, lineLft - strokeWidth * 4 * scaleX, lineTop - strokeWidth * 4 * scaleX, "#1F85DE", strokeWidth / 6, strokeWidth / 6, + const selectedLine = InteractionUtils.CreatePolyline(data, lineLft - 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 && lineBot - lineTop > 1 && lineRgt - lineLft > 1, false); @@ -170,8 +183,8 @@ export class InkingStroke extends ViewBoxBaseComponent - { formatInstance.addPoints(pts.X, pts.Y, apoints, i, controlPoints); }} pointerEvents="all" cursor="all-scroll" + { formatInstance.addPoints(pts.X, pts.Y, apoints, i, controlPoints); }} onMouseEnter={() => this.onPointerEnter(i)} onMouseLeave={this.onPointerLeave} pointerEvents="all" cursor="all-scroll" /> ); // Blue circles that allow the user to edit the curvature of the line using the selected control point as the anchor. -- cgit v1.2.3-70-g09d2 From a0207bf861908da9235a1752a723e69ecdbba734 Mon Sep 17 00:00:00 2001 From: vkalev Date: Tue, 6 Jul 2021 12:45:37 -0500 Subject: refactoring --- src/client/documents/Documents.ts | 8 +- src/client/views/GestureOverlay.tsx | 2 +- src/client/views/GlobalKeyHandler.ts | 2 +- src/client/views/InkControl.tsx | 101 -------- src/client/views/InkControls.tsx | 118 +++++++++ src/client/views/InkHandles.tsx | 84 +++++++ src/client/views/InkStroke.scss | 11 + src/client/views/InkStroke.tsx | 187 ++++++++++++++ src/client/views/InkStrokeProperties.ts | 205 +++++----------- src/client/views/InkingStroke.scss | 11 - src/client/views/InkingStroke.tsx | 271 --------------------- src/client/views/PropertiesButtons.tsx | 4 +- src/client/views/PropertiesView.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 +- 19 files changed, 478 insertions(+), 550 deletions(-) delete mode 100644 src/client/views/InkControl.tsx create mode 100644 src/client/views/InkControls.tsx create mode 100644 src/client/views/InkHandles.tsx create mode 100644 src/client/views/InkStroke.scss create mode 100644 src/client/views/InkStroke.tsx delete mode 100644 src/client/views/InkingStroke.scss delete 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 24682cbd0..188fe31c0 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, InkingStroke } from "../views/InkingStroke"; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkStroke } from "../views/InkStroke"; 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: InkingStroke, dataField: defaultDataKey }, + layout: { view: InkStroke, 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 = InkingStroke.LayoutString("data"); + I.layout = InkStroke.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 = InkingStroke.LayoutString; + layout = InkStroke.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 491bf18b2..455b3a935 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 "./InkingStroke"; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, SetActiveArrowStart, SetActiveDash, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth } from "./InkStroke"; import { DocumentView } from "./nodes/DocumentView"; import { RadialMenu } from "./nodes/RadialMenu"; import HorizontalPalette from "./Palette"; diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index cbaa706e0..c1130ce61 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -116,7 +116,7 @@ export class KeyManager { case "escape": DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; - InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlBtn = false); + InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlButton = false); CurrentUserUtils.SelectedTool = InkTool.None; var doDeselect = true; if (SnappingManager.GetIsDragging()) { diff --git a/src/client/views/InkControl.tsx b/src/client/views/InkControl.tsx deleted file mode 100644 index accb16f61..000000000 --- a/src/client/views/InkControl.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React = require("react"); -import { observable, action } from "mobx"; -import { observer } from "mobx-react"; -import { InkStrokeProperties } from "./InkStrokeProperties"; -import { setupMoveUpEvents, emptyFunction } from "../../Utils"; -import { UndoManager } from "../util/UndoManager"; -import { ControlPoint } from "../../fields/InkField"; -import { Transform } from "../util/Transform"; - -export interface InkControlProps { - control: ControlPoint; - left: number; - top: number; - scaleX: number; - scaleY: number; - strokeWidth: number; - ScreenToLocalTransform: () => Transform; -} - -@observer -export class InkControl extends React.Component { - @observable private _overControl = false; - - @action - onPointerEnter = () => { - this._overControl = true; - } - - @action - onPointerLeave = () => { - this._overControl = false; - } - - /** - * Handles the movement of a selected control point when the user clicks and drags. - * @param controlNum The index of the currently selected control point. - */ - @action - onControlDown = (e: React.PointerEvent, controlNum: 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); - return false; - }, - () => controlUndo?.end(), emptyFunction); - } - } - - /** - * Deletes the currently selected point. - */ - @action - onDelete = (e: KeyboardEvent) => { - if (["-", "Backspace", "Delete"].includes(e.key)) { - if (InkStrokeProperties.Instance?.deletePoints()) e.stopPropagation(); - } - } - - /** - * Changes the current selected control point. - */ - @action - changeCurrPoint = (i: number) => { - if (InkStrokeProperties.Instance) { - InkStrokeProperties.Instance._currPoint = i; - document.addEventListener("keydown", this.onDelete, true); - } - } - - - render() { - const control = this.props.control; - const left = this.props.left; - const top = this.props.top; - const scaleX = this.props.scaleX; - const scaleY = this.props.scaleY; - const strokeWidth = this.props.strokeWidth; - - return ( - - { this.changeCurrPoint(control.I); this.onControlDown(e, control.I); }} - onMouseEnter={this.onPointerEnter} - onMouseLeave={this.onPointerLeave} - pointerEvents="all" - cursor="default" - /> - - ) - } -} \ No newline at end of file diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx new file mode 100644 index 000000000..eeddfce4c --- /dev/null +++ b/src/client/views/InkControls.tsx @@ -0,0 +1,118 @@ +import React = require("react"); +import { observable, action } from "mobx"; +import { observer } from "mobx-react"; +import { InkStrokeProperties } from "./InkStrokeProperties"; +import { setupMoveUpEvents, emptyFunction } from "../../Utils"; +import { UndoManager } from "../util/UndoManager"; +import { ControlPoint, InkData, PointData } from "../../fields/InkField"; +import { Transform } from "../util/Transform"; + +export interface InkControlProps { + data: InkData; + addedPoints: PointData[]; + format: number[]; + ScreenToLocalTransform: () => Transform; +} + +@observer +export class InkControls extends React.Component { + // @observable private _controlPoints: ControlPoint[] = []; + @observable private _overControl = -1; + @observable private _overAddPoint = -1; + + /** + * Handles the movement of a selected control point when the user clicks and drags. + * @param controlNum The index of the currently selected control point. + */ + @action + onControlDown = (e: React.PointerEvent, controlNum: 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); + return false; + }, + () => controlUndo?.end(), emptyFunction); + } + } + + /** + * Deletes the currently selected point. + */ + @action + onDelete = (e: KeyboardEvent) => { + if (["-", "Backspace", "Delete"].includes(e.key)) { + if (InkStrokeProperties.Instance?.deletePoints()) e.stopPropagation(); + } + } + + /** + * Changes the current selected control point. + */ + @action + changeCurrPoint = (i: number) => { + if (InkStrokeProperties.Instance) { + InkStrokeProperties.Instance._currentPoint = i; + document.addEventListener("keydown", this.onDelete, true); + } + } + + @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 data = this.props.data; + const controlPoints: ControlPoint[] = []; + if (data.length >= 4) { + for (let i = 0; i <= data.length - 4; i += 4) { + controlPoints.push({ X: data[i].X, Y: data[i].Y, I: i }); + controlPoints.push({ X: data[i + 3].X, Y: data[i + 3].Y, I: i + 3 }); + } + } + const addedPoints = this.props.addedPoints; + const [left, top, scaleX, scaleY, strokeWidth, dotsize] = this.props.format; + + return ( + <> + {addedPoints.map((pts, i) => + + { InkStrokeProperties.Instance?.addPoints(pts.X, pts.Y, addedPoints, i, controlPoints); }} + onMouseEnter={() => this.onEnterAddPoint(i)} + onMouseLeave={this.onLeaveAddPoint} + pointerEvents="all" + cursor="all-scroll" + /> + + )} + {controlPoints.map((control, i) => + + { this.changeCurrPoint(control.I); this.onControlDown(e, control.I); }} + onMouseEnter={() => this.onEnterControl(i)} + onMouseLeave={this.onLeaveControl} + pointerEvents="all" + cursor="default" + /> + + )} + + ); + } +} \ No newline at end of file diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx new file mode 100644 index 000000000..c2163c124 --- /dev/null +++ b/src/client/views/InkHandles.tsx @@ -0,0 +1,84 @@ +import React = require("react"); +import { observable, action } from "mobx"; +import { observer } from "mobx-react"; +import { InkStrokeProperties } from "./InkStrokeProperties"; +import { setupMoveUpEvents, emptyFunction } from "../../Utils"; +import { UndoManager } from "../util/UndoManager"; +import { InkData, HandlePoint, HandleLine } from "../../fields/InkField"; +import { Transform } from "../util/Transform"; + +export interface InkControlProps { + data: InkData; + format: number[]; + ScreenToLocalTransform: () => Transform; +} + +@observer +export class InkHandles extends React.Component { + /** + * 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 => { + 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?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleNum); + return false; + }, () => controlUndo?.end(), emptyFunction + ); + } + } + + render() { + const formatInstance = InkStrokeProperties.Instance; + if (!formatInstance) return (null); + const data = this.props.data; + const handlePoints: HandlePoint[] = []; + const handleLines: HandleLine[] = []; + if (data.length >= 4) { + // adding first and last (single) handle lines + handleLines.push({ X1: data[0].X, Y1: data[0].Y, X2: data[1].X, Y2: data[1].Y, dot1: 0, dot2: 0 }); + handleLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 }); + for (let i = 0; i <= data.length - 4; i += 4) { + handlePoints.push({ X: data[i + 1].X, Y: data[i + 1].Y, I: i + 1, dot1: i, dot2: i === 0 ? i : i - 1 }); + handlePoints.push({ X: data[i + 2].X, Y: data[i + 2].Y, I: i + 2, dot1: i + 3, dot2: i === data.length ? i + 3 : i + 4 }); + } + for (let i = 2; i < data.length - 4; i += 4) { + handleLines.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 3].X, Y2: data[i + 3].Y, dot1: i + 1, dot2: i + 2 }); + } + } + const [left, top, scaleX, scaleY, strokeWidth, dotsize] = this.props.format; + + return ( + <> + {handlePoints.map((pts, i) => + + this.onHandleDown(e, pts.I)} + pointerEvents="all" + cursor="default" + display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} /> + )} + {handleLines.map((pts, i) => + + + )} + + ); + } +} \ No newline at end of file diff --git a/src/client/views/InkStroke.scss b/src/client/views/InkStroke.scss new file mode 100644 index 000000000..812a79bd5 --- /dev/null +++ b/src/client/views/InkStroke.scss @@ -0,0 +1,11 @@ +.inkStroke { + mix-blend-mode: multiply; + stroke-linejoin: round; + stroke-linecap: round; + overflow: visible !important; + transform-origin: top left; + + svg:not(:root) { + overflow: visible !important; + } +} diff --git a/src/client/views/InkStroke.tsx b/src/client/views/InkStroke.tsx new file mode 100644 index 000000000..9ca8bdbea --- /dev/null +++ b/src/client/views/InkStroke.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, 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 720a89334..a5c028730 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -1,5 +1,4 @@ import { action, computed, observable } from "mobx"; -import { ColorState } from 'react-color'; import { Doc, Field, Opt } from "../../fields/Doc"; import { Document } from "../../fields/documentSchemas"; import { InkField, InkData, PointData } from "../../fields/InkField"; @@ -11,80 +10,62 @@ import { undoBatch } from "../util/UndoManager"; export class InkStrokeProperties { static Instance: InkStrokeProperties | undefined; - private _lastFill = "#D0021B"; - private _lastLine = "#D0021B"; - private _lastDash = "2"; - @observable _lock = false; - @observable _controlBtn = false; - @observable _currPoint = -1; + @observable _controlButton = false; + @observable _currentPoint = -1; constructor() { InkStrokeProperties.Instance = this; } + @computed get selectedInk() { + const inks = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK); + return inks.length ? inks : undefined; + } + getField(key: string) { return this.selectedInk?.reduce((p, i) => (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt); } - @computed get selectedInk() { - const inks = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK); - return inks.length ? inks : undefined; + /** + * Helper function that enables other functions to be applied to a particular ink instance. + * @param func The inputted function. + * @param requireCurrPoint Indicates whether the current selected point is needed. + */ + applyFunction = (func: (doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { X: number, Y: number }[] | undefined, requireCurrPoint: boolean = false) => { + var appliedFunc = false; + this.selectedInk?.forEach(action(inkView => { + if (this.selectedInk?.length === 1 && (!requireCurrPoint || this._currentPoint !== -1)) { + const doc = Document(inkView.rootDoc); + if (doc.type === DocumentType.INK && doc.width && doc.height) { + const ink = Cast(doc.data, InkField)?.inkData; + if (ink) { + const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X)); + const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); + const ptsXscale = NumCast(doc._width) / (oldXrange.max - oldXrange.min); + const ptsYscale = NumCast(doc._height) / (oldYrange.max - oldYrange.min); + const newPoints = func(doc, ink, ptsXscale, ptsYscale); + if (newPoints) { + const newXrange = (xs => ({ min: Math.min(...xs), max: Math.max(...xs) }))(newPoints.map(p => p.X)); + const newYrange = (ys => ({ min: Math.min(...ys), max: Math.max(...ys) }))(newPoints.map(p => p.Y)); + doc._width = (newXrange.max - newXrange.min) * ptsXscale; + doc._height = (newYrange.max - newYrange.min) * ptsYscale; + doc.x = (oldXrange.coord + (newXrange.min - oldXrange.min) * ptsXscale); + doc.y = (oldYrange.coord + (newYrange.min - oldYrange.min) * ptsYscale); + Doc.GetProto(doc).data = new InkField(newPoints); + appliedFunc = true; + } + } + } + } + })); + return appliedFunc; } - // @computed get unFilled() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.fillColor ? true : false, true) || false; } - // @computed get unStrokd() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.color ? true : false, true) || false; } - // @computed get solidFil() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.fillColor ? true : false, true) || false; } - // @computed get solidStk() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.color && (!i.rootDoc.strokeDash || i.rootDoc.strokeDash === "0") ? true : false, true) || false; } - // @computed get dashdStk() { return !this.unStrokd && this.getField("strokeDash") || ""; } - // @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; } - // @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; } - // @computed get widthStk() { return this.getField("strokeWidth") || "1"; } - // @computed get markHead() { return this.getField("strokeStartMarker") || ""; } - // @computed get markTail() { return this.getField("strokeEndMarker") || ""; } - // @computed get shapeHgt() { return this.getField("_height"); } - // @computed get shapeWid() { return this.getField("_width"); } - // @computed get shapeXps() { return this.getField("x"); } - // @computed get shapeYps() { return this.getField("y"); } - // @computed get shapeRot() { return this.getField("rotation"); } - // set unFilled(value) { this.colorFil = value ? "" : this._lastFill; } - // set solidFil(value) { this.unFilled = !value; } - // set colorFil(value) { value && (this._lastFill = value); this.selectedInk?.forEach(i => i.rootDoc.fillColor = value ? value : undefined); } - // set colorStk(value) { value && (this._lastLine = value); this.selectedInk?.forEach(i => i.rootDoc.color = value ? value : undefined); } - // set markHead(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeStartMarker = value); } - // set markTail(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeEndMarker = value); } - // set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } - // set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } - // set dashdStk(value) { - // value && (this._lastDash = value) && (this.unStrokd = false); - // this.selectedInk?.forEach(i => i.rootDoc.strokeDash = value ? this._lastDash : undefined); - // } - // set shapeXps(value) { this.selectedInk?.forEach(i => i.rootDoc.x = Number(value)); } - // set shapeYps(value) { this.selectedInk?.forEach(i => i.rootDoc.y = Number(value)); } - // set shapeRot(value) { this.selectedInk?.forEach(i => i.rootDoc.rotation = Number(value)); } - // set widthStk(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = Number(value)); } - // set shapeWid(value) { - // this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - // const oldWidth = NumCast(i.rootDoc._width); - // i.rootDoc._width = Number(value); - // this._lock && (i.rootDoc._height = (i.rootDoc._width * NumCast(i.rootDoc._height)) / oldWidth); - // }); - // } - // set shapeHgt(value) { - // this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { - // const oldHeight = NumCast(i.rootDoc._height); - // i.rootDoc._height = Number(value); - // this._lock && (i.rootDoc._width = (i.rootDoc._height * NumCast(i.rootDoc._width)) / oldHeight); - // }); - // } - /** * Adds a new control point to the ink instance when editing its format. - * @param x The x-coordinate of the current new point. - * @param y The y-coordinate of the current new point. - * @param pts The list containing all of the points to be added in PointData form. - * @param index The index of the current new point. + * @param index The index of the new point. * @param control The list of all control points of the ink. */ @undoBatch @@ -114,7 +95,7 @@ export class InkStrokeProperties { for (var i = spNum; i < ink.length; i++) { newPoints.push({ X: ink[i].X, Y: ink[i].Y }); } - this._currPoint = -1; + this._currentPoint = -1; Doc.GetProto(doc).data = new InkField(newPoints); } } @@ -122,42 +103,6 @@ export class InkStrokeProperties { })); } - /** - * Helper function that enables other functions to be applied to a particular ink instance. - * @param func The inputted function. - * @param requireCurrPoint Indicates whether the current selected point is needed. - * @returns The applied function. - */ - applyFunction = (func: (doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { X: number, Y: number }[] | undefined, requireCurrPoint: boolean = false) => { - var appliedFunc = false; - this.selectedInk?.forEach(action(inkView => { - if (this.selectedInk?.length === 1 && (!requireCurrPoint || this._currPoint !== -1)) { - const doc = Document(inkView.rootDoc); - if (doc.type === DocumentType.INK && doc.width && doc.height) { - const ink = Cast(doc.data, InkField)?.inkData; - if (ink) { - const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X)); - const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); - const ptsXscale = NumCast(doc._width) / (oldXrange.max - oldXrange.min); - const ptsYscale = NumCast(doc._height) / (oldYrange.max - oldYrange.min); - const newPoints = func(doc, ink, ptsXscale, ptsYscale); - if (newPoints) { - const newXrange = (xs => ({ min: Math.min(...xs), max: Math.max(...xs) }))(newPoints.map(p => p.X)); - const newYrange = (ys => ({ min: Math.min(...ys), max: Math.max(...ys) }))(newPoints.map(p => p.Y)); - doc._width = (newXrange.max - newXrange.min) * ptsXscale; - doc._height = (newYrange.max - newYrange.min) * ptsYscale; - doc.x = (oldXrange.coord + (newXrange.min - oldXrange.min) * ptsXscale); - doc.y = (oldYrange.coord + (newYrange.min - oldYrange.min) * ptsYscale); - Doc.GetProto(doc).data = new InkField(newPoints); - appliedFunc = true; - } - } - } - } - })); - return appliedFunc; - } - /** * Deletes the points of the current ink instance. * @returns The changed x- and y-coordinates of the control points. @@ -165,14 +110,14 @@ export class InkStrokeProperties { @undoBatch @action deletePoints = () => this.applyFunction((doc: Doc, ink: InkData) => { - var newPoints: { X: number, Y: number }[] = []; - const toRemove = Math.floor(((this._currPoint + 2) / 4)); - for (var i = 0; i < ink.length; i++) { + const newPoints: { X: number, Y: number }[] = []; + const toRemove = Math.floor(((this._currentPoint + 2) / 4)); + 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 }); } } - this._currPoint = -1; + this._currentPoint = -1; if (newPoints.length < 4) return undefined; if (newPoints.length === 4) { const newerPoints: { X: number, Y: number }[] = []; @@ -183,7 +128,7 @@ export class InkStrokeProperties { return newerPoints; } return newPoints; - }, true); + }, true) /** * Rotates the points of the current ink instance by a certain angle degree. @@ -193,7 +138,7 @@ export class InkStrokeProperties { @undoBatch @action rotateInk = (angle: number) => { - this.applyFunction((doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { + this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X)); const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); const centerPoint = { X: (oldXrange.min + oldXrange.max) / 2, Y: (oldYrange.min + oldYrange.max) / 2 }; @@ -234,7 +179,7 @@ export class InkStrokeProperties { } } return newPoints; - }); + }) /** * Rotates the target point about the origin point for a given angle (radians). @@ -247,7 +192,7 @@ export class InkStrokeProperties { const newY = Math.sin(angle) * target.X + Math.cos(angle) * target.Y; target.X = newX + origin.X; target.Y = newY + origin.Y; - return target + return target; } /** @@ -277,54 +222,20 @@ export class InkStrokeProperties { @action moveHandle = (deltaX: number, deltaY: number, handleIndex: number) => this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { - const newPoints: { X: number, Y: number }[] = []; const order = handleIndex % 4; - let newHandlePoint = { X: 0, Y: 0 }; - - for (var i = 0; i < ink.length; i++) { - if (handleIndex === i) { - newHandlePoint = { X: ink[i].X - deltaX / xScale, Y: ink[i].Y - deltaY / yScale }; - newPoints.push({ X: newHandlePoint.X, Y: newHandlePoint.Y }); - } else { - newPoints.push({ X: ink[i].X, Y: ink[i].Y }); - } - } - + const oldHandlePoint = ink[handleIndex]; + const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale }; + ink[handleIndex] = newHandlePoint; + + // Rotating opposite handle (first and final control point only have one handle). if (handleIndex !== 1 && handleIndex !== ink.length - 2) { - const oldHandlePoint = ink[handleIndex]; let oppositeHandlePoint = order === 1 ? ink[handleIndex - 3] : ink[handleIndex + 3]; const controlPoint = order === 1 ? ink[handleIndex - 1] : ink[handleIndex + 1]; const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); - order === 1 ? newPoints[handleIndex - 3] = oppositeHandlePoint : newPoints[handleIndex + 3] = oppositeHandlePoint; + order === 1 ? ink[handleIndex - 3] = oppositeHandlePoint : ink[handleIndex + 3] = oppositeHandlePoint; } - return newPoints; - }); - - // /** - // * Changes the color of the border of the ink instance. - // * @param color The new hex value to change the border to. - // * @returns true. - // */ - // @undoBatch - // @action - // switchStk = (color: ColorState) => { - // const val = String(color.hex); - // this.colorStk = val; - // return true; - // } - - // /** - // * Changes the color of the fill of the ink instance. - // * @param color The new hex value to change the fill to. - // * @returns true. - // */ - // @undoBatch - // @action - // switchFil = (color: ColorState) => { - // const val = String(color.hex); - // this.colorFil = val; - // return true; - // } + return ink; + }) } \ No newline at end of file diff --git a/src/client/views/InkingStroke.scss b/src/client/views/InkingStroke.scss deleted file mode 100644 index f67b1779d..000000000 --- a/src/client/views/InkingStroke.scss +++ /dev/null @@ -1,11 +0,0 @@ -.inkingStroke { - mix-blend-mode: multiply; - stroke-linejoin: round; - stroke-linecap: round; - overflow: visible !important; - transform-origin: top left; - - svg:not(:root) { - overflow: visible !important; - } -} diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx deleted file mode 100644 index b2ef76882..000000000 --- a/src/client/views/InkingStroke.tsx +++ /dev/null @@ -1,271 +0,0 @@ -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 "./InkingStroke.scss"; -import { FieldView, FieldViewProps } from "./nodes/FieldView"; -import React = require("react"); -import { InkStrokeProperties } from "./InkStrokeProperties"; -import { CurrentUserUtils } from "../util/CurrentUserUtils"; -import { InkControl } from "./InkControl"; - -type InkDocument = makeInterface<[typeof documentSchema]>; -const InkDocument = makeInterface(documentSchema); - -@observer -export class InkingStroke extends ViewBoxBaseComponent(InkDocument) { - private _controlUndo?: UndoManager.Batch; - @observable private _overAddPoint = -1; - - static readonly MaskDim = 50000; - - 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 - onPointerEnter = (i: number) => { - this._overAddPoint = i; - } - - @action - onPointerLeave = () => { - this._overAddPoint = -1; - } - - - @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 a selected control point when the user clicks and drags. - * @param controlNum The index of the currently selected control point. - */ - @action - onControlDown = (e: React.PointerEvent, controlNum: 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); - return false; - }, - () => controlUndo?.end(), emptyFunction); - } - } - - /** - * Handles the movement of a selected handle point when the user clicks and drags. - * @param controlNum The index of the currently selected handle point. - */ - onHandleDown = (e: React.PointerEvent, handleNum: 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?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleNum); - return false; - }, - () => controlUndo?.end(), emptyFunction); - } - } - - /** - * 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 && InkStrokeProperties.Instance && (InkStrokeProperties.Instance._controlBtn = true))); - } - } - - render() { - TraceMobx(); - const formatInstance = InkStrokeProperties.Instance; - if (!formatInstance) return (null); - const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; - const strokeWidth = Number(this.layoutDoc.strokeWidth); - const xs = data.map(p => p.X); - const ys = data.map(p => p.Y); - const lineTop = Math.min(...ys); - const lineBot = Math.max(...ys); - const lineLft = Math.min(...xs); - const lineRgt = Math.max(...xs); - const left = lineLft - strokeWidth / 2; - const top = lineTop - strokeWidth / 2; - const right = lineRgt + strokeWidth / 2; - const bottom = lineBot + 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 points = 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 && lineBot - lineTop > 1 && lineRgt - lineLft > 1, false); - - const selectedLine = InteractionUtils.CreatePolyline(data, lineLft - 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 && lineBot - lineTop > 1 && lineRgt - lineLft > 1, false); - - // Invisible polygonal line that enables the ink to be selected by the user. - const hpoints = 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 apoints = 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); - - const controlPoints: ControlPoint[] = []; - const handlePoints: HandlePoint[] = []; - const handleLine: HandleLine[] = []; - // check the purpose of this conditional - if (data.length >= 4) { - - // create separate functions for these - for (var i = 0; i <= data.length - 4; i += 4) { - controlPoints.push({ X: data[i].X, Y: data[i].Y, I: i }); - controlPoints.push({ X: data[i + 3].X, Y: data[i + 3].Y, I: i + 3 }); - handlePoints.push({ X: data[i + 1].X, Y: data[i + 1].Y, I: i + 1, dot1: i, dot2: i === 0 ? i : i - 1 }); - handlePoints.push({ X: data[i + 2].X, Y: data[i + 2].Y, I: i + 2, dot1: i + 3, dot2: i === data.length ? i + 3 : i + 4 }); - } - - // first (single) handle line - handleLine.push({ X1: data[0].X, Y1: data[0].Y, X2: data[1].X, Y2: data[1].Y, dot1: 0, dot2: 0 }); - for (var i = 2; i < data.length - 4; i += 4) { - - handleLine.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 3].X, Y2: data[i + 3].Y, dot1: i + 1, dot2: i + 2 }); - - } - // last (single) handle line - handleLine.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 }); - - for (var i = 0; i <= data.length - 4; i += 4) { - handlePoints.push({ X: data[i + 1].X, Y: data[i + 1].Y, I: i + 1, dot1: i, dot2: i === 0 ? i : i - 1 }); - handlePoints.push({ X: data[i + 2].X, Y: data[i + 2].Y, I: i + 2, dot1: i + 3, dot2: i === data.length ? i + 3 : i + 4 }); - } - } - - // Additional points (controls) added by the user via click when editing the ink's format. - const addpoints = apoints.map((pts, i) => - - { formatInstance.addPoints(pts.X, pts.Y, apoints, i, controlPoints); }} onMouseEnter={() => this.onPointerEnter(i)} onMouseLeave={this.onPointerLeave} pointerEvents="all" cursor="all-scroll" - /> - ); - // Blue circles that allow the user to edit the curvature of the line using the selected control point as the anchor. - const handles = handlePoints.map((pts, i) => - - this.onHandleDown(e, pts.I)} pointerEvents="all" cursor="default" display={(pts.dot1 === formatInstance._currPoint || pts.dot2 === formatInstance._currPoint) ? "inherit" : "none"} /> - ); - // Control points of the ink (blue outlined squares) that are made visible to user when editing its format. - const controls = controlPoints.map((pts, i) => ); - // Set of two blue lines (each with a handle at the end) that are rendered perpendicular to the current selected point while editing. - const handleLines = handleLine.map((pts, i) => - - - ); - - 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(() => formatInstance._controlBtn = !formatInstance._controlBtn), icon: "paint-brush" }); - //cm.addItem({ description: "Format Shape...", event: this.formatShape, icon: "paint-brush" }); - } - }} - > - - {hpoints} - {points} - {formatInstance._controlBtn && this.props.isSelected() ? addpoints : ""} - {formatInstance._controlBtn && this.props.isSelected() ? handleLines : ""} - {formatInstance._controlBtn && this.props.isSelected() ? handles : ""} - {this.props.isSelected() ? selectedLine : ""} - {formatInstance._controlBtn && this.props.isSelected() ? controls : ""} - - ); - } -} - - -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 5c41a96d0..920244463 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 { InkingStroke } from './InkingStroke'; +import { InkStroke } from './InkStroke'; 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) => InkingStroke.toggleMask(dv?.layoutDoc || doc)); + return this.propertyToggleBtn("Mask", "isInkMask", on => on ? "Make plain ink" : "Make highlight mask", on => "paint-brush", (dv, doc) => InkStroke.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/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index d09d949ff..4df3e4f00 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -86,7 +86,7 @@ export class PropertiesView extends React.Component { @observable openSlideOptions: boolean = false; @observable inOptions: boolean = false; - @observable _controlBtn: boolean = false; + @observable _controlButton: boolean = false; @observable _lock: boolean = false; componentDidMount() { @@ -540,7 +540,7 @@ export class PropertiesView extends React.Component { const formatInstance = InkStrokeProperties.Instance; return !formatInstance ? (null) :
{"Edit points"}
}> -
formatInstance._controlBtn = !formatInstance._controlBtn)} style={{ backgroundColor: formatInstance._controlBtn ? "black" : "" }}> +
formatInstance._controlButton = !formatInstance._controlButton)} style={{ backgroundColor: formatInstance._controlButton ? "black" : "" }}>
diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 6e6fabd0d..65061d260 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 "../InkingStroke"; +import { ActiveFillColor, ActiveInkColor, SetActiveArrowEnd, SetActiveArrowStart, SetActiveBezierApprox, SetActiveFillColor, SetActiveInkColor, SetActiveInkWidth, ActiveArrowStart, ActiveArrowEnd } from "../InkStroke"; 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 accb80c5a..9e81a6cda 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 "../../InkingStroke"; +import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkStroke"; 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 092823603..744a9c5de 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 { InkingStroke } from "../InkingStroke"; +import { InkStroke } from "../InkStroke"; 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 ? 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); + 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); return (
{!this.props.Document || !this.props.PanelWidth() ? (null) : (
Date: Tue, 6 Jul 2021 14:27:29 -0500 Subject: added breaking of tangent handle lines by holding 'Alt' key when moving --- src/client/views/InkHandles.tsx | 32 +++++++++++++++++++++++++++----- src/client/views/InkStrokeProperties.ts | 6 +++--- src/fields/InkField.ts | 2 ++ 3 files changed, 32 insertions(+), 8 deletions(-) (limited to 'src') diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx index c2163c124..993e427b3 100644 --- a/src/client/views/InkHandles.tsx +++ b/src/client/views/InkHandles.tsx @@ -15,6 +15,8 @@ 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. @@ -24,13 +26,25 @@ export class InkHandles extends React.Component { 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); setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { - InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleNum); + InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleNum, this._brokenIndices); return false; }, () => controlUndo?.end(), emptyFunction ); } } + + @action + onBreakTangent = (e: KeyboardEvent, handleIndex: 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); + } + } + } render() { const formatInstance = InkStrokeProperties.Instance; @@ -39,15 +53,15 @@ export class InkHandles extends React.Component { const handlePoints: HandlePoint[] = []; const handleLines: HandleLine[] = []; if (data.length >= 4) { - // adding first and last (single) handle lines - handleLines.push({ X1: data[0].X, Y1: data[0].Y, X2: data[1].X, Y2: data[1].Y, dot1: 0, dot2: 0 }); - handleLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 }); for (let i = 0; i <= data.length - 4; i += 4) { handlePoints.push({ X: data[i + 1].X, Y: data[i + 1].Y, I: i + 1, dot1: i, dot2: i === 0 ? i : i - 1 }); handlePoints.push({ X: data[i + 2].X, Y: data[i + 2].Y, I: i + 2, dot1: i + 3, dot2: i === data.length ? i + 3 : i + 4 }); } + // adding first and last (single) handle lines + handleLines.push({ X1: data[0].X, Y1: data[0].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: 0 }); + handleLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, X3: data[data.length - 1].X, Y3: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 }); for (let i = 2; i < data.length - 4; i += 4) { - handleLines.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 3].X, Y2: data[i + 3].Y, dot1: i + 1, dot2: i + 2 }); + handleLines.push({ X1: data[i].X, Y1: data[i].Y, X2: data[i + 1].X, Y2: data[i + 1].Y, X3: data[i + 3].X, Y3: data[i + 3].Y, dot1: i + 1, dot2: i + 2 }); } } const [left, top, scaleX, scaleY, strokeWidth, dotsize] = this.props.format; @@ -77,6 +91,14 @@ export class InkHandles extends React.Component { stroke="#1F85DE" strokeWidth={dotsize / 8} display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} /> + )} ); diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index a5c028730..812e8ff6e 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -220,15 +220,15 @@ export class InkStrokeProperties { */ @undoBatch @action - moveHandle = (deltaX: number, deltaY: number, handleIndex: number) => + moveHandle = (deltaX: number, deltaY: number, handleIndex: number, brokenIndices: number[]) => this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { const order = handleIndex % 4; const oldHandlePoint = ink[handleIndex]; const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale }; ink[handleIndex] = newHandlePoint; - // Rotating opposite handle (first and final control point only have one handle). - if (handleIndex !== 1 && handleIndex !== ink.length - 2) { + // 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]; const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index c158dac42..485376a34 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -42,6 +42,8 @@ export interface HandleLine { Y1: number; X2: number; Y2: number; + X3: number; + Y3: number; dot1: number; dot2: number; } -- cgit v1.2.3-70-g09d2 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) : (
Date: Thu, 15 Jul 2021 01:27:43 -0500 Subject: adding new point creates tangent handle lines + snapping handle tangents not working --- src/client/views/InkControls.tsx | 12 ++- src/client/views/InkHandles.tsx | 1 - src/client/views/InkStrokeProperties.ts | 170 +++++++++++++++++++++----------- src/fields/InkField.ts | 3 - 4 files changed, 121 insertions(+), 65 deletions(-) (limited to 'src') diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx index 4d8b2c6b5..da7b0df16 100644 --- a/src/client/views/InkControls.tsx +++ b/src/client/views/InkControls.tsx @@ -30,12 +30,18 @@ export class InkControls extends React.Component { InkStrokeProperties.Instance.moveControl(0, 0, 1); const controlUndo = UndoManager.StartBatch("DocDecs set radius"); const screenScale = this.props.ScreenToLocalTransform().Scale; + const order = controlIndex % 4; + const handleIndexA = order === 2 ? controlIndex - 1 : controlIndex - 2; + const handleIndexB = order === 2 ? controlIndex + 2 : controlIndex + 1; setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex); return false; }, - () => controlUndo?.end(), emptyFunction); + () => controlUndo?.end(), action((e: PointerEvent, doubleTap: boolean | undefined) => + { if (doubleTap && InkStrokeProperties.Instance?._brokenIndices.includes(controlIndex)) { + InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB); + }})); } } @@ -112,7 +118,9 @@ export class InkControls extends React.Component { width={this._overControl === i ? strokeWidth * 1.5 : strokeWidth} strokeWidth={strokeWidth / 6} stroke="#1F85DE" fill={formatInstance?._currentPoint === control.I ? "#1F85DE" : "white"} - onPointerDown={(e) => { this.changeCurrPoint(control.I); this.onControlDown(e, control.I); }} + onPointerDown={(e) => { + this.changeCurrPoint(control.I); + this.onControlDown(e, control.I); }} onMouseEnter={() => this.onEnterControl(i)} onMouseLeave={this.onLeaveControl} pointerEvents="all" diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx index a33380221..ba3fdf9db 100644 --- a/src/client/views/InkHandles.tsx +++ b/src/client/views/InkHandles.tsx @@ -24,7 +24,6 @@ export class InkHandles extends React.Component { InkStrokeProperties.Instance.moveControl(0, 0, 1); const controlUndo = UndoManager.StartBatch("DocDecs set radius"); const screenScale = this.props.ScreenToLocalTransform().Scale; - const order = handleIndex % 4; const oppositeHandleIndex = order === 1 ? handleIndex - 3 : handleIndex + 3; const controlIndex = order === 1 ? handleIndex - 1 : handleIndex + 2; diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 4ec03c560..a3f7562e0 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -71,70 +71,109 @@ export class InkStrokeProperties { */ @undoBatch @action - 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; - if (ink) { - const newPoints: { X: number, Y: number }[] = []; - var counter = 0; - for (var k = 0; k < index; k++) { - control.forEach(pt => (pts[k].X === pt.X && pts[k].Y === pt.Y) && counter++); - } - //decide where to put the new coordinate - const spNum = Math.floor(counter / 2) * 4 + 2; - - for (var i = 0; i < spNum; i++) { - ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].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; + addPoints = (x: number, y: number, points: InkData, index: number, controls: { X: number, Y: number }[]) => { + this.applyFunction((doc: Doc, ink: InkData) => { + const newControl = { X: x, Y: y }; + const newPoints: InkData = []; + let [counter, start, end] = [0, 0, 0]; + for (let k = 0; k < points.length; k++) { + if (end === 0) { + controls.forEach((control) => { + if (control.X === points[k].X && control.Y === points[k].Y) { + if (k < index) { + counter++; + start = k; + } else if (k > index) { + end = k; } - }); - - // 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); - } + }); } } - })); + if (end === 0) end = points.length-1; + // Index of new control point with regards to the ink data. + const newIndex = Math.floor(counter / 2) * 4 + 2; + // Creating new ink data with new control point and handle points inputted. + for (let i = 0; i < ink.length; i++) { + if (i === newIndex) { + const [handleA, handleB] = this.getNewHandlePoints(points.slice(start, index+1), points.slice(index, end), newControl); + newPoints.push(handleA, newControl, newControl, handleB); + const [rightControl, rightHandle] = [points[end], ink[i]]; + const scaledVector = this.getScaledHandlePoint(false, start, end, index, rightControl, rightHandle); + rightHandle && newPoints.push({ X: rightControl.X - scaledVector.X, Y: rightControl.Y - scaledVector.Y }); + } else if (i === newIndex - 1) { + const [leftControl, leftHandle] = [points[start], ink[i]]; + const scaledVector = this.getScaledHandlePoint(true, start, end, index, leftControl, leftHandle); + leftHandle && newPoints.push({ X: leftControl.X - scaledVector.X, Y: leftControl.Y - scaledVector.Y }); + } else { + ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y }); + } + + } + // Updating the indices of the control points whose handle tangency has been broken. + this._brokenIndices = this._brokenIndices.map((control) => { + if (control >= newIndex) { + return control + 4; + } else { + return control; + } + }); + this._currentPoint = -1; + return newPoints; + }); } - 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)) }; + /** + * Scales a handle point of a control point that is adjacent to a newly added one. + * @param isLeft Determines if the current control point is on the left or right side of the newly added one. + * @param start Beginning index of curve from the left control point to the newly added one. + * @param end Final index of curve from the newly added control point to its right neighbor. + */ + getScaledHandlePoint(isLeft: boolean, start: number, end: number, index: number, control: PointData, handle: PointData) { + const prevSize = end - start; + const newSize = isLeft ? index - start : end - index; + const handleVector = { X: control.X - handle.X, Y: control.Y - handle.Y }; + const scaledVector = { X: handleVector.X * (newSize / prevSize), Y: handleVector.Y * (newSize / prevSize) }; + return scaledVector; + } + /** + * Determines the position of the handle points of a newly added control point by finding the + * tangent vectors to the split curve at the new control. Given the properties of Bézier curves, + * the tangent vector to a control point is equivalent to the first/last (depending on the direction + * of the curve) leg of the Bézier curve's derivative. + * (Source: https://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html) + * + * @param C The curve represented by all points from the previous control until the newly added point. + * @param D The curve represented by all points from the newly added point to the next control. + * @param newControl The newly added control point. + */ + getNewHandlePoints = (C: PointData[], D: PointData[], newControl: PointData) => { + const [m, n] = [C.length, D.length]; + let handleSizeA = Math.sqrt((Math.pow(newControl.X - C[0].X, 2)) + (Math.pow(newControl.Y - C[0].Y, 2))); + let handleSizeB = Math.sqrt((Math.pow(D[n-1].X - newControl.X, 2)) + (Math.pow(D[n-1].Y - newControl.Y, 2))); + if (handleSizeA < 75 && handleSizeB < 75) { + handleSizeA *= 3; + handleSizeB *= 3; + } + if (Math.abs(handleSizeA - handleSizeB) < 50) { + handleSizeA *= 5; + handleSizeB *= 5; + } else if (Math.abs(handleSizeA - handleSizeB) < 150) { + handleSizeA *= 2; + handleSizeB *= 2; + } + // Finding the last leg of the derivative curve of C. + const dC = { X: (handleSizeA / n) * (C[m-1].X - C[m-2].X), Y: (handleSizeA / n) * (C[m-1].Y - C[m-2].Y) }; + // Finding the first leg of the derivative curve of D. + const dD = { X: (handleSizeB / m) * (D[1].X - D[0].X), Y: (handleSizeB / m) * (D[1].Y - D[0].Y) }; + const handleA = { X: newControl.X - dC.X, Y: newControl.Y - dC.Y }; + const handleB = { X: newControl.X + dD.X, Y: newControl.Y + dD.Y }; return [handleA, handleB]; } /** - * Deletes the points of the current ink instance. - * @returns The changed x- and y-coordinates of the control points. + * Deletes the current control point of the selected ink instance. */ @undoBatch @action @@ -160,9 +199,8 @@ export class InkStrokeProperties { }, true) /** - * Rotates the points of the current ink instance by a certain angle degree. - * @param angle The angle at which to rotate the ink (all of its x- and y-coordinates). - * @returns The changed x- and y-coordinates of the control points. + * Rotates the entire selected ink instance. + * @param angle The angle at which to rotate the ink in radians. */ @undoBatch @action @@ -210,6 +248,18 @@ export class InkStrokeProperties { return newPoints; }) + snapHandleTangent = (controlIndex: number, handleIndexA: number, handleIndexB: number) => { + this.applyFunction((doc: Doc, ink: InkData) => { + this._brokenIndices.splice(this._brokenIndices.indexOf(controlIndex), 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 newHandleB = this.rotatePoint(handleB, controlPoint, angleDifference); + ink[handleIndexB] = newHandleB; + return ink; + }); + } + /** * Rotates the target point about the origin point for a given angle (radians). */ @@ -224,6 +274,11 @@ export class InkStrokeProperties { return target; } + /** + * Finds the angle between two inputted vectors. + * + * α = arccos(a·b / |a|·|b|), where a and b are both vectors. + */ 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); @@ -255,20 +310,17 @@ export class InkStrokeProperties { @action 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 (!this._brokenIndices.includes(controlIndex) && handleIndex !== 1 && handleIndex !== ink.length - 2) { const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); ink[oppositeHandleIndex] = oppositeHandlePoint; } - return ink; }) } \ No newline at end of file diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index 485376a34..1270a2dab 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -57,13 +57,10 @@ const strokeDataSchema = createSimpleSchema({ "*": true }); -// Holistic class representing the store of an ink. @Deserializable("ink") export class InkField extends ObjectField { @serializable(list(object(strokeDataSchema))) readonly inkData: InkData; - // inkData: InkData; - constructor(data: InkData) { super(); -- cgit v1.2.3-70-g09d2 From e3b1401b854e5f968385dcc469c02fae86308bea Mon Sep 17 00:00:00 2001 From: geireann <60007097+geireann@users.noreply.github.com> Date: Sat, 17 Jul 2021 13:51:25 -0400 Subject: added broken indices to layoutDoc :broken_heart: --- src/client/views/InkHandles.tsx | 60 +++++++++++++++++++-------------- src/client/views/InkStrokeProperties.ts | 31 +++++++++++------ src/client/views/InkingStroke.tsx | 60 +++++++++++++++++---------------- 3 files changed, 86 insertions(+), 65 deletions(-) (limited to 'src') diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx index ba3fdf9db..ffb6986f1 100644 --- a/src/client/views/InkHandles.tsx +++ b/src/client/views/InkHandles.tsx @@ -6,8 +6,12 @@ import { setupMoveUpEvents, emptyFunction } from "../../Utils"; import { UndoManager } from "../util/UndoManager"; import { InkData, HandlePoint, HandleLine } from "../../fields/InkField"; import { Transform } from "../util/Transform"; +import { Doc } from "../../fields/Doc"; +import { listSpec } from "../../fields/Schema"; +import { Cast } from "../../fields/Types"; export interface InkControlProps { + inkDoc: Doc; data: InkData; format: number[]; ScreenToLocalTransform: () => Transform; @@ -19,7 +23,7 @@ export class InkHandles extends React.Component { * 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, handleIndex: 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"); @@ -29,9 +33,9 @@ export class InkHandles extends React.Component { 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, handleIndex, oppositeHandleIndex, controlIndex); - return false; - }, () => controlUndo?.end(), emptyFunction + InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex); + return false; + }, () => controlUndo?.end(), emptyFunction ); } } @@ -43,13 +47,19 @@ export class InkHandles extends React.Component { */ @action onBreakTangent = (e: KeyboardEvent, controlIndex: number) => { + const doc: Doc = this.props.inkDoc; if (["Alt"].includes(e.key)) { - if (!InkStrokeProperties.Instance?._brokenIndices.includes(controlIndex)) { - InkStrokeProperties.Instance?._brokenIndices.push(controlIndex); + if (doc) { + let brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); + if (brokenIndices && brokenIndices.includes(controlIndex)) { + brokenIndices.push(controlIndex); + } + + doc.brokenInkIndices = brokenIndices; } } } - + render() { const formatInstance = InkStrokeProperties.Instance; if (!formatInstance) return (null); @@ -76,33 +86,33 @@ export class InkHandles extends React.Component { <> {handlePoints.map((pts, i) => - this.onHandleDown(e, pts.I)} - pointerEvents="all" - cursor="default" + onPointerDown={(e) => this.onHandleDown(e, pts.I)} + pointerEvents="all" + cursor="default" display={(pts.dot1 === formatInstance._currentPoint || pts.dot2 === formatInstance._currentPoint) ? "inherit" : "none"} /> )} {handleLines.map((pts, i) => - - )} diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index a3f7562e0..4c225210e 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -1,7 +1,9 @@ import { action, computed, observable } from "mobx"; -import { Doc, Field, Opt } from "../../fields/Doc"; +import { Doc, DocListCast, Field, Opt } from "../../fields/Doc"; import { Document } from "../../fields/documentSchemas"; import { InkField, InkData, PointData, ControlPoint } from "../../fields/InkField"; +import { List } from "../../fields/List"; +import { listSpec } from "../../fields/Schema"; import { Cast, NumCast } from "../../fields/Types"; import { DocumentType } from "../documents/DocumentTypes"; import { SelectionManager } from "../util/SelectionManager"; @@ -13,7 +15,7 @@ export class InkStrokeProperties { @observable _lock = false; @observable _controlButton = false; @observable _currentPoint = -1; - @observable _brokenIndices: number[] = []; + // @observable _brokenIndices: List = new List(); constructor() { InkStrokeProperties.Instance = this; @@ -110,14 +112,20 @@ export class InkStrokeProperties { } } + + let brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); + // Updating the indices of the control points whose handle tangency has been broken. - this._brokenIndices = this._brokenIndices.map((control) => { - if (control >= newIndex) { - return control + 4; - } else { - return control; - } - }); + if(brokenIndices){ + brokenIndices = new List(brokenIndices.map((control) => { + if (control >= newIndex) { + return control + 4; + } else { + return control; + } + })); + } + doc.brokenInkIndices = brokenIndices; this._currentPoint = -1; return newPoints; }); @@ -250,7 +258,7 @@ export class InkStrokeProperties { snapHandleTangent = (controlIndex: number, handleIndexA: number, handleIndexB: number) => { this.applyFunction((doc: Doc, ink: InkData) => { - this._brokenIndices.splice(this._brokenIndices.indexOf(controlIndex), 1); + // doc.brokenIndices.splice(this._brokenIndices.indexOf(controlIndex), 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); @@ -315,8 +323,9 @@ export class InkStrokeProperties { const controlPoint = ink[controlIndex]; const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale }; ink[handleIndex] = newHandlePoint; + let brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle). - if (!this._brokenIndices.includes(controlIndex) && handleIndex !== 1 && handleIndex !== ink.length - 2) { + if (brokenIndices && !brokenIndices.includes(controlIndex) && handleIndex !== 1 && handleIndex !== ink.length - 2) { const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); ink[oppositeHandleIndex] = oppositeHandlePoint; diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index b630c20cc..bd71aaf19 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -34,8 +34,8 @@ export class InkingStroke extends ViewBoxBaseComponent { if (this.props.isSelected(true)) { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, + setupMoveUpEvents(this, e, returnFalse, emptyFunction, action((e: PointerEvent, doubleTap: boolean | undefined) => - doubleTap && this._properties && (this._properties._controlButton = true)) + doubleTap && this._properties && (this._properties._controlButton = true)) ); } } @@ -68,6 +68,7 @@ export class InkingStroke extends ViewBoxBaseComponent p.Y)); const lineBottom = Math.max(...data.map(p => p.Y)); @@ -85,31 +86,31 @@ export class InkingStroke extends ViewBoxBaseComponent 1 && lineRight - lineLeft > 1, + 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"), + 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, + 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, "", + 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", + StrCast(this.layoutDoc.strokeDash), scaleX, scaleY, "", "none", this.props.isSelected() && strokeWidth <= 5, false); return ( @@ -126,25 +127,26 @@ export class InkingStroke extends ViewBoxBaseComponent 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(() => { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" }); } }} > - + {clickableLine} {inkLine} {this.props.isSelected() ? selectedLine : ""} - {this.props.isSelected() && this._properties?._controlButton ? + {this.props.isSelected() && this._properties?._controlButton ? <> - + - : ""} ); -- cgit v1.2.3-70-g09d2 From fabaad0251cf4f64c1af7a37a5f3d6622011c058 Mon Sep 17 00:00:00 2001 From: vkalev Date: Thu, 22 Jul 2021 11:30:18 -0400 Subject: adding broken indices to layoutDoc working --- src/client/views/InkControls.tsx | 11 ++++++----- src/client/views/InkHandles.tsx | 12 ++++++------ src/client/views/InkStrokeProperties.ts | 11 ++++------- 3 files changed, 16 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx index da7b0df16..23f22c774 100644 --- a/src/client/views/InkControls.tsx +++ b/src/client/views/InkControls.tsx @@ -16,7 +16,6 @@ export interface InkControlProps { @observer export class InkControls extends React.Component { - // @observable private _controlPoints: ControlPoint[] = []; @observable private _overControl = -1; @observable private _overAddPoint = -1; @@ -38,10 +37,12 @@ export class InkControls extends React.Component { InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex); return false; }, - () => controlUndo?.end(), action((e: PointerEvent, doubleTap: boolean | undefined) => - { if (doubleTap && InkStrokeProperties.Instance?._brokenIndices.includes(controlIndex)) { - InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB); - }})); + () => controlUndo?.end(), + emptyFunction); + // action((e: PointerEvent, doubleTap: boolean | undefined) => + // { if (doubleTap && InkStrokeProperties.Instance?._brokenIndices.includes(controlIndex)) { + // InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB); + // }})); } } diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx index ffb6986f1..28b6dd820 100644 --- a/src/client/views/InkHandles.tsx +++ b/src/client/views/InkHandles.tsx @@ -8,9 +8,10 @@ import { InkData, HandlePoint, HandleLine } from "../../fields/InkField"; import { Transform } from "../util/Transform"; import { Doc } from "../../fields/Doc"; import { listSpec } from "../../fields/Schema"; +import { List } from "../../fields/List"; import { Cast } from "../../fields/Types"; -export interface InkControlProps { +export interface InkHandlesProps { inkDoc: Doc; data: InkData; format: number[]; @@ -18,7 +19,7 @@ export interface InkControlProps { } @observer -export class InkHandles extends React.Component { +export class InkHandles extends React.Component { /** * Handles the movement of a selected handle point when the user clicks and drags. * @param handleNum The index of the currently selected handle point. @@ -50,11 +51,10 @@ export class InkHandles extends React.Component { const doc: Doc = this.props.inkDoc; if (["Alt"].includes(e.key)) { if (doc) { - let brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); - if (brokenIndices && brokenIndices.includes(controlIndex)) { + const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")) || new List; + if (brokenIndices && !brokenIndices.includes(controlIndex)) { brokenIndices.push(controlIndex); } - doc.brokenInkIndices = brokenIndices; } } @@ -73,7 +73,7 @@ export class InkHandles extends React.Component { handlePoints.push({ X: data[i + 1].X, Y: data[i + 1].Y, I: i + 1, dot1: i, dot2: i === 0 ? i : i - 1 }); handlePoints.push({ X: data[i + 2].X, Y: data[i + 2].Y, I: i + 2, dot1: i + 3, dot2: i === data.length ? i + 3 : i + 4 }); } - // adding first and last (single) handle lines + // Adding first and last (single) handle lines. handleLines.push({ X1: data[0].X, Y1: data[0].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: 0 }); handleLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[data.length - 1].X, Y2: data[data.length - 1].Y, X3: data[data.length - 1].X, Y3: data[data.length - 1].Y, dot1: data.length - 1, dot2: data.length - 1 }); for (let i = 2; i < data.length - 4; i += 4) { diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 4c225210e..f218b4fbf 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -15,7 +15,6 @@ export class InkStrokeProperties { @observable _lock = false; @observable _controlButton = false; @observable _currentPoint = -1; - // @observable _brokenIndices: List = new List(); constructor() { InkStrokeProperties.Instance = this; @@ -112,11 +111,9 @@ export class InkStrokeProperties { } } - let brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); - // Updating the indices of the control points whose handle tangency has been broken. - if(brokenIndices){ + if (brokenIndices) { brokenIndices = new List(brokenIndices.map((control) => { if (control >= newIndex) { return control + 4; @@ -283,7 +280,7 @@ export class InkStrokeProperties { } /** - * Finds the angle between two inputted vectors. + * Finds the angle (in radians) between two inputted vectors. * * α = arccos(a·b / |a|·|b|), where a and b are both vectors. */ @@ -323,9 +320,9 @@ export class InkStrokeProperties { const controlPoint = ink[controlIndex]; const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale }; ink[handleIndex] = newHandlePoint; - let brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); + const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); // 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) && handleIndex !== 1 && handleIndex !== ink.length - 2) { + if ((!brokenIndices || !brokenIndices?.includes(controlIndex)) && handleIndex !== 1 && handleIndex !== ink.length - 2) { const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); ink[oppositeHandleIndex] = oppositeHandlePoint; -- cgit v1.2.3-70-g09d2 From 44f74e1e8ee82e76d32c6e5e0812ac04bf81c16c Mon Sep 17 00:00:00 2001 From: vkalev Date: Thu, 22 Jul 2021 11:58:30 -0400 Subject: additional comments --- src/client/views/InkStrokeProperties.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index f218b4fbf..1a3585f3e 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -94,15 +94,17 @@ export class InkStrokeProperties { if (end === 0) end = points.length-1; // Index of new control point with regards to the ink data. const newIndex = Math.floor(counter / 2) * 4 + 2; - // Creating new ink data with new control point and handle points inputted. + // Creating new ink data with the new control point and handle points inputted. for (let i = 0; i < ink.length; i++) { if (i === newIndex) { const [handleA, handleB] = this.getNewHandlePoints(points.slice(start, index+1), points.slice(index, end), newControl); newPoints.push(handleA, newControl, newControl, handleB); + // Adjusting the magnitude of the left handle line of the right neighboring control point. const [rightControl, rightHandle] = [points[end], ink[i]]; const scaledVector = this.getScaledHandlePoint(false, start, end, index, rightControl, rightHandle); rightHandle && newPoints.push({ X: rightControl.X - scaledVector.X, Y: rightControl.Y - scaledVector.Y }); } else if (i === newIndex - 1) { + // Adjusting the magnitude of the right handle line of the left neighboring control point. const [leftControl, leftHandle] = [points[start], ink[i]]; const scaledVector = this.getScaledHandlePoint(true, start, end, index, leftControl, leftHandle); leftHandle && newPoints.push({ X: leftControl.X - scaledVector.X, Y: leftControl.Y - scaledVector.Y }); @@ -157,6 +159,8 @@ export class InkStrokeProperties { const [m, n] = [C.length, D.length]; let handleSizeA = Math.sqrt((Math.pow(newControl.X - C[0].X, 2)) + (Math.pow(newControl.Y - C[0].Y, 2))); let handleSizeB = Math.sqrt((Math.pow(D[n-1].X - newControl.X, 2)) + (Math.pow(D[n-1].Y - newControl.Y, 2))); + // Scaling adjustments to improve the ratio between the magnitudes of the two handle lines. + // (Ensures that the new point added doesn't augment the inital shape of the curve much). if (handleSizeA < 75 && handleSizeB < 75) { handleSizeA *= 3; handleSizeB *= 3; -- cgit v1.2.3-70-g09d2