import React = require("react"); import { action, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../fields/Doc"; import { ControlPoint, InkData, PointData, InkField } from "../../fields/InkField"; import { List } from "../../fields/List"; import { listSpec } from "../../fields/Schema"; import { Cast, NumCast } from "../../fields/Types"; import { setupMoveUpEvents, returnFalse } from "../../Utils"; import { Transform } from "../util/Transform"; import { UndoManager } from "../util/UndoManager"; import { Colors } from "./global/globalEnums"; import { InkingStroke } from "./InkingStroke"; import { InkStrokeProperties } from "./InkStrokeProperties"; import { DocumentView } from "./nodes/DocumentView"; import { SelectionManager } from "../util/SelectionManager"; export interface InkControlProps { inkDoc: Doc; inkView: DocumentView; inkCtrlPoints: InkData; screenCtrlPoints: InkData; screenSpaceLineWidth: number; ScreenToLocalTransform: () => Transform; nearestScreenPt: () => PointData | undefined; } @observer export class InkControlPtHandles extends React.Component { @observable private _overControl = -1; @observable controlUndo: UndoManager.Batch | undefined; componentDidMount() { document.addEventListener("keydown", this.onDelete, true); } componentWillUnmount() { document.removeEventListener("keydown", this.onDelete, true); } /** * Handles the movement of a selected control point when the user clicks and drags. * @param controlIndex The index of the currently selected control point. */ @action onControlDown = (e: React.PointerEvent, controlIndex: number): void => { const ptFromScreen = this.props.inkView.ComponentView?.ptFromScreen; if (ptFromScreen) { const order = controlIndex % 4; 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")); const wasSelected = InkStrokeProperties.Instance._currentPoint === controlIndex; if (!wasSelected) InkStrokeProperties.Instance._currentPoint = -1; const origInk = this.props.inkCtrlPoints.slice(); setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { if (!this.controlUndo) this.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(this.props.inkView, inkMoveEnd.X - inkMoveStart.X, inkMoveEnd.Y - inkMoveStart.Y, controlIndex, origInk); return false; }), action(() => { if (this.controlUndo) { InkStrokeProperties.Instance.snapControl(this.props.inkView, controlIndex); } this.controlUndo?.end(); this.controlUndo = undefined; UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); }), action((e: PointerEvent, doubleTap: boolean | undefined) => { const equivIndex = controlIndex === 0 ? this.props.inkCtrlPoints.length - 1 : controlIndex === this.props.inkCtrlPoints.length - 1 ? 0 : controlIndex; if (doubleTap || e.button === 2) { if (!brokenIndices?.includes(equivIndex) && !brokenIndices?.includes(controlIndex)) { if (brokenIndices) brokenIndices.push(controlIndex); else this.props.inkDoc.brokenInkIndices = new List([controlIndex]); } else { if (brokenIndices?.includes(equivIndex)) { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth"); InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, equivIndex, handleIndexA, handleIndexB); } if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth"); InkStrokeProperties.Instance.snapHandleTangent(this.props.inkView, controlIndex, handleIndexA, handleIndexB); } } this.controlUndo?.end(); this.controlUndo = undefined; } this.changeCurrPoint(controlIndex); }), undefined, undefined, () => wasSelected && this.changeCurrPoint(-1)); } } /** * Updates whether a user has hovered over a particular control point or point that could be added * on click. */ @action onEnterControl = (i: number) => { this._overControl = i; }; @action onLeaveControl = () => { this._overControl = -1; }; /** * Deletes the currently selected point. */ @action onDelete = (e: KeyboardEvent) => { if (["-", "Backspace", "Delete"].includes(e.key)) { InkStrokeProperties.Instance.deletePoints(this.props.inkView, e.shiftKey); e.stopPropagation(); } } /** * Changes the current selected control point. */ @action changeCurrPoint = (i: number) => InkStrokeProperties.Instance._currentPoint = i render() { // Accessing the current ink's data and extracting all control points. const scrData = this.props.screenCtrlPoints; const sreenCtrlPoints: ControlPoint[] = []; for (let i = 0; i <= scrData.length - 4; i += 4) { sreenCtrlPoints.push({ ...scrData[i], I: i }); sreenCtrlPoints.push({ ...scrData[i + 3], I: i + 3 }); } const inkData = this.props.inkCtrlPoints; const inkCtrlPts: ControlPoint[] = []; for (let i = 0; i <= inkData.length - 4; i += 4) { inkCtrlPts.push({ ...inkData[i], I: i }); inkCtrlPts.push({ ...inkData[i + 3], I: i + 3 }); } const closed = InkingStroke.IsClosed(inkData); const nearestScreenPt = this.props.nearestScreenPt(); const TagType = (broken?: boolean) => broken ? "rect" : "circle"; const hdl = (control: { X: number, Y: number, I: number }, scale: number, color: string) => { 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)} onMouseEnter={() => this.onEnterControl(control.I)} onMouseLeave={this.onLeaveControl} pointerEvents="all" cursor="default" />; }; return ( {!nearestScreenPt ? (null) : } {sreenCtrlPoints.map(control => hdl(control, this._overControl !== control.I ? 1 : 3 / 2, Colors.WHITE))} ); } } 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(SelectionManager.Views(), scaling, p2(), v1n, e.shiftKey); InkStrokeProperties.Instance.rotateInk(SelectionManager.Views(), angle, p2()); return false; }, action(() => { this.controlUndo?.end(); this.controlUndo = undefined; UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); }), returnFalse); } render() { const hdl = (key: string, pt: PointData, dragFunc: (e: React.PointerEvent) => void) => this._overStart = false)} onPointerEnter={action(() => this._overStart = true)} onPointerDown={dragFunc} pointerEvents="all" />; return ( {hdl("start", this.props.startPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.startPt, () => this.props.endPt))} {hdl("end", this.props.endPt, (e: React.PointerEvent) => this.dragRotate(e, () => this.props.endPt, () => this.props.startPt))} ); } }