import React = require("react"); import { action, observable } from "mobx"; import { observer } from "mobx-react"; import { Doc } from "../../fields/Doc"; import { ControlPoint, InkData, PointData } from "../../fields/InkField"; import { listSpec } from "../../fields/Schema"; import { Cast } from "../../fields/Types"; import { setupMoveUpEvents } from "../../Utils"; import { Transform } from "../util/Transform"; import { UndoManager } from "../util/UndoManager"; import { Colors } from "./global/globalEnums"; import { InkStrokeProperties } from "./InkStrokeProperties"; import { List } from "../../fields/List"; import { InkingStroke } from "./InkingStroke"; export interface InkControlProps { inkDoc: Doc; 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 => { if (InkStrokeProperties.Instance) { const screenScale = this.props.ScreenToLocalTransform().Scale; 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; setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt"); InkStrokeProperties.Instance?.moveControlPtHandle(delta[0] * screenScale, delta[1] * screenScale, controlIndex); return false; }), action(() => { if (this.controlUndo) { InkStrokeProperties.Instance?.snapControl(this.props.inkDoc, 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(equivIndex, handleIndexA, handleIndexB); } if (equivIndex !== controlIndex && brokenIndices?.includes(controlIndex)) { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("make smooth"); InkStrokeProperties.Instance?.snapHandleTangent(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(); e.stopPropagation(); } } /** * Changes the current selected control point. */ @action changeCurrPoint = (i: number) => { if (InkStrokeProperties.Instance) { 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 screenSpaceLineWidth = this.props.screenSpaceLineWidth; 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))} ); } }