aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2021-11-29 14:09:17 -0500
committerbobzel <zzzman@gmail.com>2021-11-29 14:09:17 -0500
commit061dab5285d3a334a258d8097a6e95b065b30de3 (patch)
tree0f77cdacae4330e0588538b3ba5f3445616a0530
parent7e6baacfe6c1b55a3fe1807903455f9ff3844d74 (diff)
added moving ink stroke segments. added stretching and rotating ink strokes about opposite end point.
-rw-r--r--src/client/util/InteractionUtils.tsx5
-rw-r--r--src/client/views/InkControlPtHandles.tsx77
-rw-r--r--src/client/views/InkStrokeProperties.ts24
-rw-r--r--src/client/views/InkingStroke.tsx79
4 files changed, 150 insertions, 35 deletions
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 (<svg fill={color}> {/* setting the svg fill sets the arrowStart fill */}
+ return (<svg fill={color} onPointerDown={downHdlr}> {/* setting the svg fill sets the arrowStart fill */}
{nodefs ? (null) : <defs>
{arrowStart !== "dot" && arrowEnd !== "dot" ? (null) :
<marker id={`dot${defGuid}`} orient="auto" markerUnits="userSpaceOnUse" refX={0} refY="0" overflow="visible">
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<InkControlProps> {
inkCtrlPts.push({ ...inkData[i + 3], I: i + 3 });
}
- const screenSpaceLineWidth = this.props.screenSpaceLineWidth;
const closed = InkingStroke.IsClosed(inkData);
const nearestScreenPt = this.props.nearestScreenPt();
const TagType = (broken?: boolean) => broken ? "rect" : "circle";
@@ -141,18 +140,18 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"))?.includes(control.I);
const Tag = TagType((control.I === 0 || control.I === inkData.length - 1) && !closed) as keyof JSX.IntrinsicElements;
return <Tag key={control.I.toString() + scale}
- x={control.X - screenSpaceLineWidth * 2 * scale}
- y={control.Y - screenSpaceLineWidth * 2 * scale}
+ x={control.X - this.props.screenSpaceLineWidth * 2 * scale}
+ y={control.Y - this.props.screenSpaceLineWidth * 2 * scale}
cx={control.X}
cy={control.Y}
- r={screenSpaceLineWidth * 2 * scale}
+ r={this.props.screenSpaceLineWidth * 2 * scale}
opacity={this.controlUndo ? 0.15 : 1}
- height={screenSpaceLineWidth * 4 * scale}
- width={screenSpaceLineWidth * 4 * scale}
- strokeWidth={screenSpaceLineWidth / 2}
+ height={this.props.screenSpaceLineWidth * 4 * scale}
+ width={this.props.screenSpaceLineWidth * 4 * scale}
+ strokeWidth={this.props.screenSpaceLineWidth / 2}
stroke={Colors.MEDIUM_BLUE}
fill={broken ? Colors.MEDIUM_BLUE : color}
- onPointerDown={(e: any) => this.onControlDown(e, control.I)}
+ onPointerDown={(e: React.PointerEvent) => this.onControlDown(e, control.I)}
onMouseEnter={() => this.onEnterControl(control.I)}
onMouseLeave={this.onLeaveControl}
pointerEvents="all"
@@ -164,7 +163,7 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
<circle key={"npt"}
cx={nearestScreenPt.X}
cy={nearestScreenPt.Y}
- r={screenSpaceLineWidth * 2}
+ r={this.props.screenSpaceLineWidth * 2}
fill={"#00007777"}
stroke={"#00007777"}
strokeWidth={0}
@@ -175,4 +174,62 @@ export class InkControlPtHandles extends React.Component<InkControlProps> {
</svg>
);
}
+}
+
+
+export interface InkEndProps {
+ inkDoc: Doc;
+ inkView: DocumentView;
+ screenSpaceLineWidth: number;
+ startPt: PointData;
+ endPt: PointData;
+}
+@observer
+export class InkEndPtHandles extends React.Component<InkEndProps> {
+ @observable controlUndo: UndoManager.Batch | undefined;
+ @observable _overStart: boolean = false;
+ @observable _overEnd: boolean = false;
+
+ @action
+ dragRotate = (e: React.PointerEvent, p1: () => { X: number, Y: number }, p2: () => { X: number, Y: number }) => {
+ setupMoveUpEvents(this, e, (e) => {
+ if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("stretch ink");
+ // compute stretch factor by finding scaling along axis between start and end points
+ const v1 = { x: p1().X - p2().X, y: p1().Y - p2().Y };
+ const v2 = { x: e.clientX - p2().X, y: e.clientY - p2().Y };
+ const v1len = Math.sqrt(v1.x * v1.x + v1.y * v1.y);
+ const v2len = Math.sqrt(v2.x * v2.x + v2.y * v2.y);
+ const scaling = v2len / v1len;
+ const v1n = { x: v1.x / v1len, y: v1.y / v1len };
+ const v2n = { x: v2.x / v2len, y: v2.y / v2len };
+ const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y)
+ InkStrokeProperties.Instance?.stretchInk([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) => <circle key={"npt"}
+ cx={pt.X}
+ cy={pt.Y}
+ r={this.props.screenSpaceLineWidth * 2}
+ fill={this._overStart ? "#aaaaaa" : "#99999977"}
+ stroke={"#00007777"}
+ strokeWidth={0}
+ onPointerLeave={action(() => this._overStart = false)}
+ onPointerEnter={action(() => this._overStart = true)}
+ onPointerDown={e => dragFunc(e)}
+ pointerEvents="all"
+ />;
+ return (<svg>
+ {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))}
+ </svg>
+ );
+ }
} \ 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
@@ -178,6 +178,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.
*/
@undoBatch
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<FieldViewProps, InkDocume
/**
* Handles the movement of the entire ink object when the user clicks and drags.
*/
+ @action
onPointerDown = (e: React.PointerEvent) => {
+ 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<FieldViewProps, InkDocume
this._nearestScrPt = nearestPt;
}
-
nearestScreenPt = () => this._nearestScrPt;
componentUI = (boundsLeft: number, boundsTop: number) => {
const inkDoc = this.props.Document;
@@ -200,11 +228,17 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
- return SnappingManager.GetIsDragging() ? (null) : <div className="inkstroke-UI" style={{
- clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)`
- }} >
- {!this._properties?._controlButton ? (null) :
- <>
+ return SnappingManager.GetIsDragging() ? (null) :
+ !this._properties?._controlButton ?
+ (!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? (null) :
+ <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
+ <InkEndPtHandles
+ inkView={this.props.docViewPath().lastElement()}
+ inkDoc={inkDoc}
+ startPt={this.ptToScreen(inkData[0])}
+ endPt={this.ptToScreen(inkData.lastElement())}
+ screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} /></div>) :
+ <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
{InteractionUtils.CreatePolyline(screenPts, 0, 0, Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth,
StrCast(inkDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), StrCast(inkDoc.strokeBezier),
"none", startMarker, endMarker, StrCast(inkDoc.strokeDash), 1, 1, "", "none", 1.0, false)}
@@ -222,8 +256,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
screenCtrlPoints={screenHdlPts}
screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth}
ScreenToLocalTransform={this.props.ScreenToLocalTransform} />
- </>}
- </div>;
+ </div>;
}
render() {
@@ -246,11 +279,12 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== "transparent" ? StrCast(this.layoutDoc.color, "transparent") : "transparent") :
["transparent", "rgb(68, 118, 247)", "rgb(68, 118, 247)", "yellow", "magenta", "cyan", "orange"][highlightIndex];
// Invisible polygonal line that enables the ink to be selected by the user.
- const clickableLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor,
- inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && fillColor && (new Color(fillColor)).alpha() < 1 ? 6 : 15),
+ const clickableLine = (downHdlr?: (e: React.PointerEvent) => 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 <div className="inkStroke-wrapper" style={{ display: "flex", alignItems: "center", height: "100%" }}>
@@ -266,7 +300,6 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
}}
onPointerLeave={action(e => this._nearestScrPt = undefined)}
onPointerMove={this.props.isSelected() ? this.onPointerMove : undefined}
- onPointerDown={this.onPointerDown}
onClick={e => this._handledClick && e.stopPropagation()}
onContextMenu={() => {
const cm = ContextMenu.Instance;
@@ -275,7 +308,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
cm?.addItem({ description: "Edit Points", event: action(() => { if (this._properties) { this._properties._controlButton = !this._properties._controlButton; } }), icon: "paint-brush" });
}}
>
- {clickableLine}
+ {clickableLine(this.onPointerDown)}
{inkLine}
</svg>
{!closed ? (null) :