aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/InkingStroke.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/InkingStroke.tsx')
-rw-r--r--src/client/views/InkingStroke.tsx174
1 files changed, 91 insertions, 83 deletions
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx
index 8ff080f81..0b3619b22 100644
--- a/src/client/views/InkingStroke.tsx
+++ b/src/client/views/InkingStroke.tsx
@@ -1,3 +1,25 @@
+/*
+ InkingStroke - a document that represents an individual vector stroke drawn as a Bezier curve (open or closed) and optionally filled.
+
+ The primary data is:
+ data - an InkField which is an array of PointData (X,Y values). The data is laid out as a sequence of simple bezier segments:
+ point 1, tangent pt 1, tangent pt 2, point 2, point 3, tangent pt 3, ... (Note that segment endpoints are duplicated ie Point2 = Point 3)
+ brokenIndices - an array of indexes into the data field where the incoming and outgoing tangents are not constrained to be equal
+ text - a text field that will be centered within a closed ink stroke
+ isInkMask - a flag that makes the ink stroke render as a mask over its collection where the stroke itself is mixBlendMode multiplied by
+ the underlying collection content, and everything outside the stroke is covered by a semi-opaque dark gray mask.
+
+ The coordinates of the ink data need to be mapped to the screen since ink points are not changed when the DocumentView is translated or scaled.
+ Thus the mapping can roughly be described by:
+ the Top/Left of the ink data (minus 1/2 the ink width) maps to the Top/Left of the DocumentView
+ the Width/Height of the ink data (minus the ink width) is scaled to the PanelWidth/PanelHeight of the documentView
+ NOTE: use ptToScreen() and ptFromScreen() to transform between ink and screen space
+
+ InkStrokes have a specialized 'componentUI' method that is called by MainView to render all of the interactive editing controls in
+ screen space (to avoid scaling artifacts)
+
+ Most of the operations that can be performed on an InkStroke (eg delete a point, rotate, stretch) are implemented in the InkStrokeProperties helper class
+*/
import React = require("react");
import { action, IReactionDisposer, observable, reaction } from "mobx";
import { observer } from "mobx-react";
@@ -30,35 +52,32 @@ const InkDocument = makeInterface(documentSchema);
@observer
export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) {
+ static readonly MaskDim = 50000; // choose a really big number to make sure mask fits over container (which in theory can be arbitrarily big)
public static LayoutString(fieldStr: string) { return FieldView.LayoutString(InkingStroke, fieldStr); }
- static readonly MaskDim = 50000;
public static IsClosed(inkData: InkData) {
return inkData && inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y;
}
- @observable private _properties?: InkStrokeProperties;
- _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
- _selDisposer: IReactionDisposer | undefined;
+ private _handledClick = false; // flag denoting whether ink stroke has handled a psuedo-click onPointerUp so that the real onClick event can be stopPropagated
+ private _selDisposer?: IReactionDisposer;
- @observable _nearestT: number | undefined;
- @observable _nearestSeg: number | undefined;
- @observable _nearestScrPt: { X: number, Y: number } | undefined;
- @observable _inkSamplePts: { X: number, Y: number }[] | undefined;
-
- constructor(props: FieldViewProps & InkDocument) {
- super(props);
-
- this._properties = InkStrokeProperties.Instance;
- }
+ @observable _nearestSeg?: number; // nearest Bezier segment along the ink stroke to the cursor (used for displaying the Add Point highlight)
+ @observable _nearestT?: number; // nearest t value within the nearest Bezier segment "
+ @observable _nearestScrPt?: { X: number, Y: number }; // nearst screen point on the ink stroke ""
componentDidMount() {
this.props.setContentView?.(this);
this._selDisposer = reaction(() => this.props.isSelected(), // react to stroke being deselected by turning off ink handles
- selected => !selected && this.toggleControlButton());
+ selected => !selected && (InkStrokeProperties.Instance._controlButton = false));
}
componentWillUnmount() {
this._selDisposer?.();
}
+ /**
+ * @returns the center of the ink stroke in the ink document's coordinate space (not screen space, and not the ink data coordinate space);
+ * DocumentDecorations calls getBounds() on DocumentViews which call getCenter() if defined - in the case of ink it needs to be defined since
+ * the center of the ink stroke changes as the stroke is rotated.
+ */
getCenter = (xf: Transform) => {
const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData();
const angle = -NumCast(this.layoutDoc.rotation);
@@ -80,6 +99,12 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
CognitiveServices.Inking.Appliers.ConcatenateHandwriting(this.dataDoc, ["inkAnalysis", "handwriting"], [data]);
}
+ /**
+ * Toggles whether the ink stroke is displayed as an overlay mask or as a regular stroke.
+ * When displayed as a mask, the stroke is rendered with mixBlendMode set to multiply so that the stroke will
+ * appear to illuminate what it covers up. At the same time, all pixels that are not under the stroke will be
+ * dimmed by a semi-opaque overlay mask.
+ */
public static toggleMask = action((inkDoc: Doc) => {
inkDoc.isInkMask = !inkDoc.isInkMask;
inkDoc._backgroundColor = inkDoc.isInkMask ? "rgba(0,0,0,0.7)" : undefined;
@@ -88,70 +113,47 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
inkDoc._stayInCollection = inkDoc.isInkMask ? true : undefined;
});
/**
- * Handles the movement of the entire ink object when the user clicks and drags.
+ * Drags the a simple bezier segment of the stroke.
+ * Also adds a control point when double clicking on the stroke.
*/
@action
onPointerDown = (e: React.PointerEvent) => {
- const ptFromScreen = this.ptFromScreen;
this._handledClick = false;
- 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 (isEditing) {
- this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
+ 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 = InkStrokeProperties.Instance._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 = this.ptFromScreen({ X: delta[0], Y: delta[1] });
+ const inkMoveStart = this.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(() => {
+ 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) {
+ InkStrokeProperties.Instance._controlButton = true;
+ 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
+ if (isEditing) {
+ this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance.addPoints(this.props.docViewPath().lastElement(), this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice());
}
- }), isEditing, isEditing, action(() => wasSelected && InkStrokeProperties.Instance && (InkStrokeProperties.Instance._currentPoint = -1)));
- }
- }
-
- /**
- * Ensures the ink controls and handles aren't rendered when the current ink stroke is reselected.
- */
- @action
- toggleControlButton = () => {
- if (!this.props.isSelected() && this._properties) {
- this._properties._controlButton = false;
- }
- }
-
- @action
- checkHighlighter = () => {
- if (CurrentUserUtils.SelectedTool === InkTool.Highlighter) {
- // this._previousColor = ActiveInkColor();
- SetActiveInkColor("rgba(245, 230, 95, 0.75)");
- }
+ }
+ }), isEditing, isEditing, action(() => wasSelected && (InkStrokeProperties.Instance._currentPoint = -1)));
}
ptFromScreen = (scrPt: { X: number, Y: number }) => {
@@ -173,13 +175,23 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
return { X: scrPt[0], Y: scrPt[1] };
}
+ /**
+ * Snaps a screen space point to this stroke, optionally skipping bezier segments indicated by 'excludeSegs'
+ * @param scrPt - the point to snap to this stroke
+ * @param excludeSegs - optional segments in this stroke to skip (this is used when dragging a point on the stroke and not wanting the drag point to snap to its neighboring segments)
+ *
+ * @returns the nearest ink space point on this stroke to the screen point AND the screen space distance from the snapped point to the nearest point
+ */
snapPt = (scrPt: { X: number, Y: number }, excludeSegs?: number[]) => {
const { inkData } = this.inkScaledData();
- const inkPt = this.ptFromScreen(scrPt);
- const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, inkPt, excludeSegs ?? []);
+ const { nearestPt, distance } = InkStrokeProperties.nearestPtToStroke(inkData, this.ptFromScreen(scrPt), excludeSegs ?? []);
return { nearestPt, distance: distance * this.props.ScreenToLocalTransform().inverse().Scale };
}
+ /**
+ * extracts key features from the inkData, including: the data points, the ink width, the ink bounds (top,left, width, height), and the scale
+ * factor for converting between ink and screen space.
+ */
inkScaledData = () => {
const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? [];
const inkStrokeWidth = NumCast(this.rootDoc.strokeWidth, 1);
@@ -229,7 +241,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
const startMarker = StrCast(this.layoutDoc.strokeStartMarker);
const endMarker = StrCast(this.layoutDoc.strokeEndMarker);
return SnappingManager.GetIsDragging() ? (null) :
- !this._properties?._controlButton ?
+ !InkStrokeProperties.Instance._controlButton ?
(!this.props.isSelected() || InkingStroke.IsClosed(inkData) ? (null) :
<div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}>
<InkEndPtHandles
@@ -287,15 +299,11 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
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%" }}>
+ return <div className="inkStroke-wrapper">
<svg className="inkStroke"
style={{
- width: "100%",
- height: "100%",
- pointerEvents: "none",
transform: this.props.Document.isInkMask ? `translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined,
mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? "multiply" : "unset",
- overflow: "visible",
cursor: this.props.isSelected() ? "default" : undefined
}}
onPointerLeave={action(e => this._nearestScrPt = undefined)}
@@ -305,7 +313,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume
const cm = ContextMenu.Instance;
!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" });
+ cm?.addItem({ description: "Edit Points", event: action(() => InkStrokeProperties.Instance._controlButton = !InkStrokeProperties.Instance._controlButton), icon: "paint-brush" });
}}
>
{clickableLine(this.onPointerDown)}