aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/InkControlPtHandles.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'src/client/views/InkControlPtHandles.tsx')
-rw-r--r--src/client/views/InkControlPtHandles.tsx174
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