diff options
Diffstat (limited to 'src/client/views/InkingStroke.tsx')
| -rw-r--r-- | src/client/views/InkingStroke.tsx | 239 |
1 files changed, 153 insertions, 86 deletions
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 5fc159f14..752db1413 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -1,34 +1,46 @@ import React = require("react"); -import { action, observable } from "mobx"; +import { Bezier } from "bezier-js"; +import { action, IReactionDisposer, observable, reaction } 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 { Cast, NumCast, StrCast, BoolCast } from "../../fields/Types"; import { TraceMobx } from "../../fields/util"; -import { setupMoveUpEvents, emptyFunction, returnFalse } from "../../Utils"; +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 { InkControlPtHandles } from "./InkControlPtHandles"; 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"; -import { Colors } from "./global/globalEnums"; -import { GestureOverlay } from "./GestureOverlay"; +import { InkTangentHandles } from "./InkTangentHandles"; +import { FieldView, FieldViewProps } from "./nodes/FieldView"; +import { SnappingManager } from "../util/SnappingManager"; +import Color = require("color"); type InkDocument = makeInterface<[typeof documentSchema]>; const InkDocument = makeInterface(documentSchema); @observer export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocument>(InkDocument) { + 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; + + @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); @@ -36,8 +48,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume this._properties = InkStrokeProperties.Instance; } - public static LayoutString(fieldStr: string) { - return FieldView.LayoutString(InkingStroke, fieldStr); + 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()); + } + componentWillUnmount() { + this._selDisposer?.(); } analyzeStrokes() { @@ -52,15 +69,23 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume 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) => { + this._handledClick = false; if (this.props.isSelected(true)) { setupMoveUpEvents(this, e, returnFalse, emptyFunction, - action((e: PointerEvent, doubleTap: boolean | undefined) => - doubleTap && this._properties && (this._properties._controlButton = true)) + 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) { + this._nearestT && this._nearestSeg !== undefined && InkStrokeProperties.Instance?.addPoints(this._nearestT, this._nearestSeg, this.inkScaledData().inkData.slice()); + } + }), this._properties?._controlButton, this._properties?._controlButton ); } } @@ -75,80 +100,140 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume } } + @action + checkHighlighter = () => { + if (CurrentUserUtils.SelectedTool === InkTool.Highlighter) { + // this._previousColor = ActiveInkColor(); + SetActiveInkColor("rgba(245, 230, 95, 0.75)"); + } + } + + inkScaledData = () => { + const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; + const inkStrokeWidth = NumCast(this.rootDoc.strokeWidth, 1); + const inkTop = Math.min(...inkData.map(p => p.Y)) - inkStrokeWidth / 2; + const inkBottom = Math.max(...inkData.map(p => p.Y)) + inkStrokeWidth / 2; + const inkLeft = Math.min(...inkData.map(p => p.X)) - inkStrokeWidth / 2; + const inkRight = Math.max(...inkData.map(p => p.X)) + inkStrokeWidth / 2; + const inkWidth = Math.max(1, inkRight - inkLeft); + const inkHeight = Math.max(1, inkBottom - inkTop); + return { + inkData, + inkStrokeWidth, + inkTop, + inkLeft, + inkWidth, + inkHeight, + inkScaleX: ((this.props.PanelWidth() - inkStrokeWidth) / ((inkWidth - inkStrokeWidth) || 1) || 1), + inkScaleY: ((this.props.PanelHeight() - inkStrokeWidth) / ((inkHeight - inkStrokeWidth) || 1) || 1) + }; + } + + @action + onPointerMove = (e: React.PointerEvent) => { + 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 { distance, nearestT, nearestSeg, nearestPt } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY }); + + this._nearestT = nearestT; + this._nearestSeg = nearestSeg; + this._nearestScrPt = nearestPt; + } + + + nearestScreenPt = () => this._nearestScrPt; + componentUI = (boundsLeft: number, boundsTop: number) => { + const inkDoc = this.props.Document; + const screenSpaceCenterlineStrokeWidth = 3; // the width of the blue line widget that shows the centerline of the ink stroke + const { inkData, inkScaleX, inkScaleY, inkStrokeWidth, inkTop, inkLeft } = this.inkScaledData(); + + const screenInkWidth = this.props.ScreenToLocalTransform().inverse().transformDirection(inkStrokeWidth, inkStrokeWidth); + 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 screenHdlPts = screenPts; + + 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) : + <> + {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)} + <InkControlPtHandles + inkDoc={inkDoc} + inkCtrlPoints={inkData} + screenCtrlPoints={screenHdlPts} + nearestScreenPt={this.nearestScreenPt} + screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} /> + <InkTangentHandles + inkDoc={inkDoc} + screenCtrlPoints={screenHdlPts} + screenSpaceLineWidth={screenSpaceCenterlineStrokeWidth} + ScreenToLocalTransform={this.props.ScreenToLocalTransform} /> + </>} + </div>; + } + render() { TraceMobx(); - this.toggleControlButton(); - // Extracting the ink data and formatting information of the current ink stroke. - // console.log(InkingStroke.InkShape); - const InkShape = GestureOverlay.Instance.InkShape; - const data: InkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; - const inkDoc: Doc = this.layoutDoc; - 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; + const { inkData, inkStrokeWidth, inkLeft, inkTop, inkScaleX, inkScaleY, inkWidth, inkHeight } = this.inkScaledData(); + + const startMarker = StrCast(this.layoutDoc.strokeStartMarker); + const endMarker = StrCast(this.layoutDoc.strokeEndMarker); + const closed = InkingStroke.IsClosed(inkData); + const fillColor = StrCast(this.layoutDoc.fillColor, "transparent"); + const strokeColor = !closed && fillColor && fillColor !== "transparent" ? fillColor : StrCast(this.layoutDoc.color); // 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 inkLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, strokeColor, inkStrokeWidth, inkStrokeWidth, + StrCast(this.layoutDoc.strokeLineJoin), StrCast(this.layoutDoc.strokeLineCap), + StrCast(this.layoutDoc.strokeBezier), !closed ? "none" : fillColor === "transparent" ? "none" : fillColor, startMarker, endMarker, + StrCast(this.layoutDoc.strokeDash), inkScaleX, inkScaleY, "", "none", 1.0, false); // Thin blue line indicating that the current ink stroke is selected. - const selectedLine = InteractionUtils.CreatePolyline(data, left, top, Colors.MEDIUM_BLUE, strokeWidth, 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); + // const selectedLine = InteractionUtils.CreatePolyline(data, left, top, Colors.MEDIUM_BLUE, strokeWidth, 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", 1.0, false); // Invisible polygonal line that enables the ink to be selected by the user. - const clickableLine = InteractionUtils.CreatePolyline(data, left, top, "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 highlightIndex = BoolCast(this.props.Document.isLinkButton) && Doc.isBrushedHighlightedDegree(this.props.Document); // bcz: Argh!! need to identify a tree view doc better than a LayoutTemlatString + const highlightColor = !highlightIndex ? + 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]; + + const clickableLine = InteractionUtils.CreatePolyline(inkData, inkLeft, inkTop, highlightColor, + inkStrokeWidth, inkStrokeWidth + (highlightIndex && closed && (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); // 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 ( <svg className="inkStroke" style={{ - pointerEvents: this.props.Document.isInkMask && this.props.layerProvider?.(this.props.Document) !== false ? "all" : "none", + 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)} + onPointerMove={this.props.isSelected() ? this.onPointerMove : undefined} onPointerDown={this.onPointerDown} + onClick={e => this._handledClick && e.stopPropagation()} onContextMenu={() => { 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" }); - } + !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 ? - <> - <InkControls - inkDoc={inkDoc} - data={data} - addedPoints={addedPoints} - format={[left, top, scaleX, scaleY, strokeWidth]} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} /> - <InkHandles - inkDoc={inkDoc} - data={data} - shape={InkShape} - format={[left, top, scaleX, scaleY, strokeWidth]} - ScreenToLocalTransform={this.props.ScreenToLocalTransform} /> - </> : ""} </svg> ); } @@ -168,23 +253,5 @@ export function ActiveFillColor(): string { return StrCast(ActiveInkPen()?.activ 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 ActiveInkWidth(): number { return Number(ActiveInkPen()?.activeInkWidth); } 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); }); |
