diff options
author | bobzel <zzzman@gmail.com> | 2021-09-27 23:03:47 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2021-09-27 23:03:47 -0400 |
commit | 1561e37eb966607564938530a71aeb7e3ba27583 (patch) | |
tree | dd48aa670f8fce0291d25dfe4c5dc48de7f7486e | |
parent | eb529611c97c9936577697b829c50b4ca0736c6e (diff) |
fixed ink stroke editing to work with closed curves (eg break / restore tangent & drag start/end point). changed ink rendering to use svg bezier curves. fixed bugs with keydown handler to break tangent.
-rw-r--r-- | src/client/util/InteractionUtils.tsx | 51 | ||||
-rw-r--r-- | src/client/views/GestureOverlay.tsx | 4 | ||||
-rw-r--r-- | src/client/views/InkControls.tsx | 10 | ||||
-rw-r--r-- | src/client/views/InkHandles.tsx | 38 | ||||
-rw-r--r-- | src/client/views/InkStrokeProperties.ts | 11 | ||||
-rw-r--r-- | src/client/views/InkingStroke.tsx | 4 |
6 files changed, 53 insertions, 65 deletions
diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 4a8011e3c..66afc849e 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -146,7 +146,7 @@ export namespace InteractionUtils { if (shape) { //if any of the shape are true pts = makePolygon(shape, points); } - else if ((points.length >= 5 && points[3].X === points[4].X) || (points.length === 4)) { + else if (((points.length >= 5 && points[3].X === points[4].X) || (points.length === 4)) && !bezier) { for (var i = 0; i < points.length - 3; i += 4) { const array = [[points[i].X, points[i].Y], [points[i + 1].X, points[i + 1].Y], [points[i + 2].X, points[i + 2].Y], [points[i + 3].X, points[i + 3].Y]]; for (var t = 0; t < 1; t += 0.01) { @@ -154,47 +154,22 @@ export namespace InteractionUtils { pts.push({ X: point[0], Y: point[1] }); } } - } - else if (points.length > 1 && points[points.length - 1].X === points[0].X && points[points.length - 1].Y === points[0].Y) { - //pointer is up (first and last points are the same) - const newPoints = points.reduce((p, pts) => { p.push([pts.X, pts.Y]); return p; }, [] as number[][]); - newPoints.pop(); - - const bezierCurves = fitCurve(newPoints, parseInt(bezier)); - for (const curve of bezierCurves) { - for (var t = 0; t < 1; t += 0.01) { - const point = beziercurve(t, curve); - pts.push({ X: point[0], Y: point[1] }); - } - } } else { pts = points.slice(); - // bcz: Ugh... this is ugly, but shapes apprently have an extra point added that is = (p[0].x,p[0].y+1) as some sort of flag. need to remove it here. - if (pts.length > 2 && pts[pts.length - 2].X === pts[0].X && pts[pts.length - 2].Y === pts[0].Y) { - pts.pop(); - } - } - if (isNaN(scalex)) { - scalex = 1; - } - if (isNaN(scaley)) { - scaley = 1; } - const strpts = pts.reduce((acc: string, pt: { X: number, Y: number }) => acc + - `${(pt.X - left - width / 2) * scalex + width / 2}, - ${(pt.Y - top - width / 2) * scaley + width / 2} `, ""); + if (isNaN(scalex)) scalex = 1; + if (isNaN(scaley)) scaley = 1; + + const toScr = (p: { X: number, Y: number }) => ` ${(p.X - left - width / 2) * scalex + width / 2}, ${(p.Y - top - width / 2) * scaley + width / 2} `; + var strpts = bezier ? + pts.reduce((acc: string, pt, i) => acc + (i % 4 !== 0 ? "" : "M" + toScr(pts[i]) + "C" + toScr(pts[i + 1]) + toScr(pts[i + 2]) + toScr(pts[i + 3])), "") : + pts.reduce((acc: string, pt) => acc + `${toScr(pt)} `, ""); + const dashArray = dash && Number(dash) ? String(Number(width) * Number(dash)) : undefined; const defGuid = Utils.GenerateGuid(); const arrowDim = Math.max(0.5, 8 / Math.log(Math.max(2, strokeWidth))); - const addables = pts.map((pts, i) => - <svg height="10" width="10"> - <circle cx={(pts.X - left - width / 2) * scalex + width / 2} cy={(pts.Y - top - width / 2) * scaley + width / 2} r={strokeWidth / 2} stroke="black" strokeWidth={1} fill="blue" - onDoubleClick={(e) => { console.log(i); }} pointerEvents="all" cursor="all-scroll" - /> - </svg>); - - + const Tag = (bezier ? "path" : "polyline") as keyof JSX.IntrinsicElements; return (<svg fill={color}> {/* setting the svg fill sets the arrowStart fill */} {nodefs ? (null) : <defs> {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) : <marker id={`dot${defGuid}`} orient="auto" overflow="visible"> @@ -207,8 +182,10 @@ export namespace InteractionUtils { <polygon points={`${2 - arrowDim} ${-Math.max(1, arrowDim / 2)}, ${2 - arrowDim} ${Math.max(1, arrowDim / 2)}, 3 0`} /> </marker>} </defs>} - <polyline - points={strpts} + + <Tag + d={bezier ? strpts : undefined} + points={bezier ? undefined : strpts} style={{ // filter: drawHalo ? "url(#inkSelectionHalo)" : undefined, fill: fill && fill !== "transparent" ? fill : "none", diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 6ccbd3fd7..465e6b2df 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -708,7 +708,7 @@ export class GestureOverlay extends Touchable { this._points.push({ X: left, Y: top }); break; - + case "triangle": this._points.push({ X: left, Y: bottom }); this._points.push({ X: left, Y: bottom }); @@ -851,7 +851,7 @@ export class GestureOverlay extends Touchable { }), this._points.length <= 1 ? (null) : <svg key="svg" width={B.width} height={B.height} style={{ transform: `translate(${B.left}px, ${B.top}px)`, pointerEvents: "none", position: "absolute", zIndex: 30000, overflow: "visible" }}> - {InteractionUtils.CreatePolyline(this._points.map(p => ({ X: p.X, Y: p.Y - (rect?.y || 0) })), B.left, B.top, ActiveInkColor(), width, width, ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)} + {InteractionUtils.CreatePolyline(this._points.map(p => ({ X: p.X, Y: p.Y - (rect?.y || 0) })), B.left, B.top, ActiveInkColor(), width, width, "", ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), 1, 1, this.InkShape, "none", 1.0, false)} </svg>] ]; } diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx index 4df7ee813..7e685288d 100644 --- a/src/client/views/InkControls.tsx +++ b/src/client/views/InkControls.tsx @@ -37,8 +37,8 @@ export class InkControls extends React.Component<InkControlProps> { 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; + const handleIndexA = ((order === 3 ? controlIndex - 1 : controlIndex - 2) + this.props.inkCtrlPoints.length) % this.props.inkCtrlPoints.length; + const handleIndexB = (order === 3 ? controlIndex + 2 : controlIndex + 1) % this.props.inkCtrlPoints.length; const brokenIndices = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number")); setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { @@ -47,7 +47,11 @@ export class InkControls extends React.Component<InkControlProps> { }, () => controlUndo?.end(), action((e: PointerEvent, doubleTap: boolean | undefined) => { - if (doubleTap && brokenIndices && brokenIndices.includes(controlIndex)) { + const equivIndex = controlIndex === 0 ? this.props.inkCtrlPoints.length - 1 : controlIndex === this.props.inkCtrlPoints.length - 1 ? 0 : controlIndex; + if (doubleTap && brokenIndices?.includes(equivIndex)) { + InkStrokeProperties.Instance?.snapHandleTangent(equivIndex, handleIndexA, handleIndexB); + } + if (doubleTap && brokenIndices?.includes(controlIndex)) { InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB); } })); diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx index afe94cdfb..1a514bdce 100644 --- a/src/client/views/InkHandles.tsx +++ b/src/client/views/InkHandles.tsx @@ -31,10 +31,11 @@ export class InkHandles extends React.Component<InkHandlesProps> { 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; - document.addEventListener("keydown", (e: KeyboardEvent) => this.onBreakTangent(e, controlIndex), true); + const oppositeHandleRawIndex = order === 1 ? handleIndex - 3 : handleIndex + 3; + const oppositeHandleIndex = (oppositeHandleRawIndex < 0 ? this.props.data.length + oppositeHandleRawIndex : oppositeHandleRawIndex) % this.props.data.length; + const controlIndex = (order === 1 ? handleIndex - 1 : handleIndex + 2) % this.props.data.length; setupMoveUpEvents(this, e, (e: PointerEvent, down: number[], delta: number[]) => { + if (e.altKey) this.onBreakTangent(controlIndex); InkStrokeProperties.Instance?.moveHandle(-delta[0] * screenScale, -delta[1] * screenScale, handleIndex, oppositeHandleIndex, controlIndex); return false; }, () => controlUndo?.end(), emptyFunction @@ -48,15 +49,14 @@ export class InkHandles extends React.Component<InkHandlesProps> { * @param handleNum The index of the currently selected handle point. */ @action - onBreakTangent = (e: KeyboardEvent, controlIndex: number) => { - const doc: Doc = this.props.inkDoc; - if (["Alt"].includes(e.key)) { - e.stopPropagation(); - if (doc) { - const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")) || new List; - if (brokenIndices && !brokenIndices.includes(controlIndex)) { - brokenIndices.push(controlIndex); - } + onBreakTangent = (controlIndex: number) => { + const doc = this.props.inkDoc; + if (doc) { + const closed = this.props.data.lastElement().X === this.props.data[0].X && this.props.data.lastElement().Y === this.props.data[0].Y; + const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")) || new List; + if (!brokenIndices?.includes(controlIndex) && + ((controlIndex > 0 && controlIndex < this.props.data.length - 1) || closed)) { + brokenIndices.push(controlIndex); doc.brokenInkIndices = brokenIndices; } } @@ -70,14 +70,20 @@ export class InkHandles extends React.Component<InkHandlesProps> { const data = this.props.data; const handlePoints: HandlePoint[] = []; const handleLines: HandleLine[] = []; + const closed = data.lastElement().X === data[0].X && data.lastElement().Y === data[0].Y; if (data.length >= 4) { 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 }); + handlePoints.push({ X: data[i + 1].X, Y: data[i + 1].Y, I: i + 1, dot1: i, dot2: i === 0 ? (closed ? data.length - 1 : 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 ? (closed ? (i + 4) % 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 }); + if (closed) { + handleLines.push({ X1: data[data.length - 2].X, Y1: data[data.length - 2].Y, X2: data[0].X, Y2: data[0].Y, X3: data[1].X, Y3: data[1].Y, dot1: 0, dot2: data.length - 1 }); + } + else { + 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 + 1].X, Y2: data[i + 1].Y, X3: data[i + 3].X, Y3: data[i + 3].Y, dot1: i + 1, dot2: i + 2 }); } diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 42190238e..2073497b9 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -1,14 +1,14 @@ import { action, computed, observable, reaction } from "mobx"; -import { Doc, DocListCast, Field, Opt } from "../../fields/Doc"; +import { Doc, Field, Opt } from "../../fields/Doc"; import { Document } from "../../fields/documentSchemas"; -import { InkField, InkData, PointData, ControlPoint, InkTool } from "../../fields/InkField"; +import { InkData, InkField, InkTool, PointData } from "../../fields/InkField"; import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; import { Cast, NumCast } from "../../fields/Types"; import { DocumentType } from "../documents/DocumentTypes"; +import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { SelectionManager } from "../util/SelectionManager"; import { undoBatch } from "../util/UndoManager"; -import { CurrentUserUtils } from "../util/CurrentUserUtils"; export class InkStrokeProperties { static Instance: InkStrokeProperties | undefined; @@ -241,6 +241,7 @@ export class InkStrokeProperties { this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { const newPoints: { X: number, Y: number }[] = []; const order = controlIndex % 4; + const closed = ink.lastElement().X === ink[0].X && ink.lastElement().Y === ink[0].Y; for (var i = 0; i < ink.length; i++) { const leftHandlePoint = order === 0 && i === controlIndex + 1; const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2; @@ -248,6 +249,7 @@ export class InkStrokeProperties { leftHandlePoint || rightHandlePoint || (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || + ((order === 0 || order === 3) && (controlIndex === 0 || controlIndex === ink.length - 1) && (i === 1 || i === ink.length - 2) && closed) || (order === 3 && i === controlIndex - 1) || (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) || (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) || @@ -335,6 +337,7 @@ 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 closed = ink.lastElement().X === ink[0].X && ink.lastElement().Y === ink[0].Y; const oldHandlePoint = ink[handleIndex]; let oppositeHandlePoint = ink[oppositeHandleIndex]; const controlPoint = ink[controlIndex]; @@ -342,7 +345,7 @@ export class InkStrokeProperties { ink[handleIndex] = newHandlePoint; 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)) && (closed || (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 ca39bdaa1..a518bf07b 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -11,14 +11,11 @@ import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { InteractionUtils } from "../util/InteractionUtils"; -import { Scripting } from "../util/Scripting"; import { ContextMenu } from "./ContextMenu"; import { ViewBoxBaseComponent } from "./DocComponent"; import { Colors } from "./global/globalEnums"; import { InkControls } from "./InkControls"; import { InkHandles } from "./InkHandles"; -import { GestureOverlay } from "./GestureOverlay"; -import { isThisTypeNode } from "typescript"; import "./InkStroke.scss"; import { InkStrokeProperties } from "./InkStrokeProperties"; import { FieldView, FieldViewProps } from "./nodes/FieldView"; @@ -83,6 +80,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick; if (doubleTap && this._properties) { this._properties._controlButton = true; + InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1); this._handledClick = true; // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView } }), this._properties?._controlButton, this._properties?._controlButton |