diff options
Diffstat (limited to 'src/client/views/InkControlPtHandles.tsx')
-rw-r--r-- | src/client/views/InkControlPtHandles.tsx | 174 |
1 files changed, 174 insertions, 0 deletions
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx new file mode 100644 index 000000000..0644488b3 --- /dev/null +++ b/src/client/views/InkControlPtHandles.tsx @@ -0,0 +1,174 @@ +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<InkControlProps> { + + @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<number>([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 <Tag key={control.I.toString() + scale} + x={control.X - screenSpaceLineWidth * 2 * scale} + y={control.Y - screenSpaceLineWidth * 2 * scale} + cx={control.X} + cy={control.Y} + r={screenSpaceLineWidth * 2 * scale} + opacity={this.controlUndo ? 0.15 : 1} + height={screenSpaceLineWidth * 4 * scale} + width={screenSpaceLineWidth * 4 * scale} + strokeWidth={screenSpaceLineWidth / 2} + stroke={Colors.MEDIUM_BLUE} + fill={broken ? Colors.MEDIUM_BLUE : color} + onPointerDown={(e: any) => this.onControlDown(e, control.I)} + onMouseEnter={() => this.onEnterControl(control.I)} + onMouseLeave={this.onLeaveControl} + pointerEvents="all" + cursor="default" + />; + }; + return (<svg> + {!nearestScreenPt ? (null) : + <circle key={"npt"} + cx={nearestScreenPt.X} + cy={nearestScreenPt.Y} + r={screenSpaceLineWidth * 2} + fill={"#00007777"} + stroke={"#00007777"} + strokeWidth={0} + pointerEvents="none" + /> + } + {sreenCtrlPoints.map(control => hdl(control, this._overControl !== control.I ? 1 : 3 / 2, Colors.WHITE))} + </svg> + ); + } +}
\ No newline at end of file |