From 061dab5285d3a334a258d8097a6e95b065b30de3 Mon Sep 17 00:00:00 2001 From: bobzel Date: Mon, 29 Nov 2021 14:09:17 -0500 Subject: added moving ink stroke segments. added stretching and rotating ink strokes about opposite end point. --- src/client/util/InteractionUtils.tsx | 5 +- src/client/views/InkControlPtHandles.tsx | 77 +++++++++++++++++++++++++++---- src/client/views/InkStrokeProperties.ts | 24 ++++++++++ src/client/views/InkingStroke.tsx | 79 ++++++++++++++++++++++---------- 4 files changed, 150 insertions(+), 35 deletions(-) (limited to 'src') diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index a32a8eecc..4eb0be320 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -91,7 +91,8 @@ export namespace InteractionUtils { export function CreatePolyline(points: { X: number, Y: number }[], left: number, top: number, color: string, width: number, strokeWidth: number, lineJoin: string, lineCap: string, bezier: string, fill: string, arrowStart: string, arrowEnd: string, - dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean) { + dash: string | undefined, scalex: number, scaley: number, shape: string, pevents: string, opacity: number, nodefs: boolean, + downHdlr?: ((e: React.PointerEvent) => void)) { const pts = shape ? makePolygon(shape, points) : points; if (isNaN(scalex)) scalex = 1; @@ -107,7 +108,7 @@ export namespace InteractionUtils { const Tag = (bezier ? "path" : "polyline") as keyof JSX.IntrinsicElements; const makerStrokeWidth = strokeWidth / 2; - return ( {/* setting the svg fill sets the arrowStart fill */} + return ( {/* setting the svg fill sets the arrowStart fill */} {nodefs ? (null) : {arrowStart !== "dot" && arrowEnd !== "dot" ? (null) : diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx index f24dab949..df803ba31 100644 --- a/src/client/views/InkControlPtHandles.tsx +++ b/src/client/views/InkControlPtHandles.tsx @@ -6,7 +6,7 @@ import { ControlPoint, InkData, PointData, InkField } from "../../fields/InkFiel import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; import { Cast, NumCast } from "../../fields/Types"; -import { setupMoveUpEvents } from "../../Utils"; +import { setupMoveUpEvents, returnFalse } from "../../Utils"; import { Transform } from "../util/Transform"; import { UndoManager } from "../util/UndoManager"; import { Colors } from "./global/globalEnums"; @@ -133,7 +133,6 @@ export class InkControlPtHandles extends React.Component { inkCtrlPts.push({ ...inkData[i + 3], I: i + 3 }); } - const screenSpaceLineWidth = this.props.screenSpaceLineWidth; const closed = InkingStroke.IsClosed(inkData); const nearestScreenPt = this.props.nearestScreenPt(); const TagType = (broken?: boolean) => broken ? "rect" : "circle"; @@ -141,18 +140,18 @@ export class InkControlPtHandles extends React.Component { const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"))?.includes(control.I); const Tag = TagType((control.I === 0 || control.I === inkData.length - 1) && !closed) as keyof JSX.IntrinsicElements; return this.onControlDown(e, control.I)} + onPointerDown={(e: React.PointerEvent) => this.onControlDown(e, control.I)} onMouseEnter={() => this.onEnterControl(control.I)} onMouseLeave={this.onLeaveControl} pointerEvents="all" @@ -164,7 +163,7 @@ export class InkControlPtHandles extends React.Component { { ); } +} + + +export interface InkEndProps { + inkDoc: Doc; + inkView: DocumentView; + screenSpaceLineWidth: number; + startPt: PointData; + endPt: PointData; +} +@observer +export class InkEndPtHandles extends React.Component { + @observable controlUndo: UndoManager.Batch | undefined; + @observable _overStart: boolean = false; + @observable _overEnd: boolean = false; + + @action + dragRotate = (e: React.PointerEvent, p1: () => { X: number, Y: number }, p2: () => { X: number, Y: number }) => { + setupMoveUpEvents(this, e, (e) => { + if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("stretch ink"); + // compute stretch factor by finding scaling along axis between start and end points + const v1 = { x: p1().X - p2().X, y: p1().Y - p2().Y }; + const v2 = { x: e.clientX - p2().X, y: e.clientY - p2().Y }; + const v1len = Math.sqrt(v1.x * v1.x + v1.y * v1.y); + const v2len = Math.sqrt(v2.x * v2.x + v2.y * v2.y); + const scaling = v2len / v1len; + const v1n = { x: v1.x / v1len, y: v1.y / v1len }; + const v2n = { x: v2.x / v2len, y: v2.y / v2len }; + const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y) + InkStrokeProperties.Instance?.stretchInk([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n); + InkStrokeProperties.Instance?.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y }); + return false; + }, action(() => { + this.controlUndo?.end(); + this.controlUndo = undefined; + UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); + }), returnFalse); + } + + render() { + const hdl = (pt: PointData, dragFunc: (e: React.PointerEvent) => void) => this._overStart = false)} + onPointerEnter={action(() => this._overStart = true)} + onPointerDown={e => dragFunc(e)} + pointerEvents="all" + />; + return ( + {hdl(this.props.startPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.startPt, () => this.props.endPt))} + {hdl(this.props.endPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.endPt, () => this.props.startPt))} + + ); + } } \ No newline at end of file diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 02288bbb5..8ad4864f9 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -177,6 +177,30 @@ export class InkStrokeProperties { }); } + /** + * Rotates ink stroke(s) about a point + * @param inkStrokes set of ink documentViews to rotate + * @param angle The angle at which to rotate the ink in radians. + * @param scrpt The center point of the rotation in screen coordinates + */ + @undoBatch + @action + stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: { x: number, y: number }, scrVec: { x: number, y: number }) => { + this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => { + const ptFromScreen = view.ComponentView?.ptFromScreen; + const ptToScreen = view.ComponentView?.ptToScreen; + return !ptToScreen || !ptFromScreen ? ink : + ink.map(i => ptToScreen(i)).map(i => { + const pvec = { X: i.X - scrpt.x, Y: i.Y - scrpt.y }; + const svec = pvec.X * scrVec.x * scaling + pvec.Y * scrVec.y * scaling; + const ovec = -pvec.X * scrVec.y + pvec.Y * (scrVec.x); + const newscrpt = { X: scrpt.x + svec * scrVec.x - ovec * scrVec.y, Y: scrpt.y + svec * scrVec.y + ovec * scrVec.x }; + const newpt = ptFromScreen!(newscrpt); + return newpt; + }); + }); + } + /** * Handles the movement/scaling of a control point. */ diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 59efb36dd..ecc82a580 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,28 +1,29 @@ import React = require("react"); import { action, IReactionDisposer, observable, reaction } from "mobx"; import { observer } from "mobx-react"; -import { Doc, HeightSym, WidthSym } from "../../fields/Doc"; +import { Doc, WidthSym } from "../../fields/Doc"; import { documentSchema } from "../../fields/documentSchemas"; import { InkData, InkField, InkTool } from "../../fields/InkField"; import { makeInterface } from "../../fields/Schema"; import { BoolCast, Cast, NumCast, StrCast } from "../../fields/Types"; import { TraceMobx } from "../../fields/util"; -import { emptyFunction, returnFalse, setupMoveUpEvents, OmitKeys } from "../../Utils"; +import { OmitKeys, returnFalse, setupMoveUpEvents } from "../../Utils"; import { CognitiveServices } from "../cognitive_services/CognitiveServices"; import { CurrentUserUtils } from "../util/CurrentUserUtils"; import { InteractionUtils } from "../util/InteractionUtils"; import { SnappingManager } from "../util/SnappingManager"; +import { Transform } from "../util/Transform"; +import { UndoManager } from "../util/UndoManager"; import { ContextMenu } from "./ContextMenu"; import { ViewBoxBaseComponent } from "./DocComponent"; import { Colors } from "./global/globalEnums"; -import { InkControlPtHandles } from "./InkControlPtHandles"; +import { InkControlPtHandles, InkEndPtHandles } from "./InkControlPtHandles"; import "./InkStroke.scss"; import { InkStrokeProperties } from "./InkStrokeProperties"; import { InkTangentHandles } from "./InkTangentHandles"; import { FieldView, FieldViewProps } from "./nodes/FieldView"; -import Color = require("color"); -import { Transform } from "../util/Transform"; import { FormattedTextBox } from "./nodes/formattedText/FormattedTextBox"; +import Color = require("color"); type InkDocument = makeInterface<[typeof documentSchema]>; const InkDocument = makeInterface(documentSchema); @@ -89,21 +90,49 @@ export class InkingStroke extends ViewBoxBaseComponent { + const ptFromScreen = this.ptFromScreen; this._handledClick = false; - if (this.props.isSelected(true)) { - setupMoveUpEvents(this, e, returnFalse, emptyFunction, + if (InkStrokeProperties.Instance && ptFromScreen) { + const inkView = this.props.docViewPath().lastElement(); + const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); + const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint( + (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, + (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] })); + const { nearestSeg } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY }); + const controlIndex = nearestSeg; + const wasSelected = InkStrokeProperties.Instance?._currentPoint === controlIndex; + var controlUndo: UndoManager.Batch | undefined; + const isEditing = this._properties?._controlButton && this.props.isSelected(); + setupMoveUpEvents(this, e, + !isEditing ? returnFalse : action((e: PointerEvent, down: number[], delta: number[]) => { + if (!controlUndo) controlUndo = UndoManager.StartBatch("drag ink ctrl pt"); + const inkMoveEnd = ptFromScreen({ X: delta[0], Y: delta[1] }); + const inkMoveStart = ptFromScreen({ X: 0, Y: 0 }); + InkStrokeProperties.Instance?.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex); + InkStrokeProperties.Instance?.moveControlPtHandle(inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex + 3); + return false; + }), + !isEditing ? returnFalse : action(() => { + if (controlUndo) { + InkStrokeProperties.Instance?.snapControl(inkView, controlIndex); + InkStrokeProperties.Instance?.snapControl(inkView, controlIndex + 3); + } + controlUndo?.end(); + controlUndo = undefined; + UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); + }), action((e: PointerEvent, doubleTap: boolean | undefined) => { doubleTap = doubleTap || this.props.docViewPath().lastElement()?.docView?._pendingDoubleClick; if (doubleTap && this._properties) { this._properties._controlButton = true; InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1); this._handledClick = true; // mark the double-click pseudo pointerevent so we can block the real mouse event from propagating to DocumentView - } else if (this._properties?._controlButton) { + } else if (isEditing) { this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice()); } - }), this._properties?._controlButton, this._properties?._controlButton - ); + }), isEditing, isEditing, () => wasSelected && InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1)); } } @@ -185,7 +214,6 @@ export class InkingStroke extends ViewBoxBaseComponent this._nearestScrPt; componentUI = (boundsLeft: number, boundsTop: number) => { const inkDoc = this.props.Document; @@ -200,11 +228,17 @@ export class InkingStroke extends ViewBoxBaseComponent - {!this._properties?._controlButton ? (null) : - <> + return SnappingManager.GetIsDragging() ? (null) : + !this._properties?._controlButton ? + (!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? (null) : +
+
) : +
{InteractionUtils.CreatePolyline(screenPts, 0, 0, Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth, StrCast(inkDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(inkDoc.strokeBezier), "none", startMarker, endMarker, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)} @@ -222,8 +256,7 @@ export class InkingStroke extends ViewBoxBaseComponent - } -
; + ; } render() { @@ -246,11 +279,12 @@ export class InkingStroke extends ViewBoxBaseComponent void) => InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor, + inkStrokeWidth, inkStrokeWidth + (CurrentUserUtils.SelectedTool === InkTool.Eraser ? 0 : highlightIndex && closed && fillColor && (new Color(fillColor)).alpha() < 1 ? 6 : 15), StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker, - undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0, false); + undefined, inkScaleX, inkScaleY, "", this.props.pointerEvents ?? (this.props.layerProvider?.(this.props.Document) === false ? "none" : "visiblepainted"), 0.0, + false, downHdlr); // Set of points rendered upon the ink that can be added if a user clicks on one. return
@@ -266,7 +300,6 @@ export class InkingStroke extends ViewBoxBaseComponent this._nearestScrPt = undefined)} onPointerMove={this.props.isSelected() ? this.onPointerMove : undefined} - onPointerDown={this.onPointerDown} onClick={e => this._handledClick && e.stopPropagation()} onContextMenu={() => { const cm = ContextMenu.Instance; @@ -275,7 +308,7 @@ export class InkingStroke extends ViewBoxBaseComponent { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" }); }} > - {clickableLine} + {clickableLine(this.onPointerDown)} {inkLine} {!closed ? (null) : -- cgit v1.2.3-70-g09d2