import { action, computed, trace, observable, runInAction } from "mobx"; import { observer } from "mobx-react"; import { Utils } from "../../Utils"; import { Transform } from "../util/Transform"; import "./InkingCanvas.scss"; import { InkingControl } from "./InkingControl"; import { InkingStroke } from "./InkingStroke"; import React = require("react"); import { undoBatch, UndoManager } from "../util/UndoManager"; import { StrokeData, InkField, InkTool } from "../../new_fields/InkField"; import { Doc } from "../../new_fields/Doc"; import { Cast, PromiseValue, NumCast } from "../../new_fields/Types"; interface InkCanvasProps { getScreenTransform: () => Transform; AnnotationDocument: Doc; Document: Doc; inkFieldKey: string; children: () => JSX.Element[]; } @observer export class InkingCanvas extends React.Component { maxCanvasDim = 8192 / 2; // 1/2 of the maximum canvas dimension for Chrome @observable inkMidX: number = 0; @observable inkMidY: number = 0; private previousState?: Map; private _currentStrokeId: string = ""; public static IntersectStrokeRect(stroke: StrokeData, selRect: { left: number, top: number, width: number, height: number }): boolean { return stroke.pathData.reduce((inside: boolean, val) => inside || (selRect.left < val.x && selRect.left + selRect.width > val.x && selRect.top < val.y && selRect.top + selRect.height > val.y) , false); } public static StrokeRect(stroke: StrokeData): { left: number, top: number, right: number, bottom: number } { return stroke.pathData.reduce((bounds: { left: number, top: number, right: number, bottom: number }, val) => ({ left: Math.min(bounds.left, val.x), top: Math.min(bounds.top, val.y), right: Math.max(bounds.right, val.x), bottom: Math.max(bounds.bottom, val.y) }) , { left: Number.MAX_VALUE, top: Number.MAX_VALUE, right: -Number.MAX_VALUE, bottom: -Number.MAX_VALUE }); } componentDidMount() { PromiseValue(Cast(this.props.AnnotationDocument[this.props.inkFieldKey], InkField)).then(ink => runInAction(() => { if (ink) { let bounds = Array.from(ink.inkData).reduce(([mix, max, miy, may], [id, strokeData]) => strokeData.pathData.reduce(([mix, max, miy, may], p) => [Math.min(mix, p.x), Math.max(max, p.x), Math.min(miy, p.y), Math.max(may, p.y)], [mix, max, miy, may]), [Number.MAX_VALUE, Number.MIN_VALUE, Number.MAX_VALUE, Number.MIN_VALUE]); this.inkMidX = (bounds[0] + bounds[1]) / 2; this.inkMidY = (bounds[2] + bounds[3]) / 2; } })); } @computed get inkData(): Map { let map = Cast(this.props.AnnotationDocument[this.props.inkFieldKey], InkField); return !map ? new Map : new Map(map.inkData); } set inkData(value: Map) { this.props.AnnotationDocument[this.props.inkFieldKey] = new InkField(value); } @action onPointerDown = (e: React.PointerEvent): void => { if (e.button !== 0 || e.altKey || e.ctrlKey || InkingControl.Instance.selectedTool === InkTool.None) { return; } document.addEventListener("pointermove", this.onPointerMove, true); document.addEventListener("pointerup", this.onPointerUp, true); e.stopPropagation(); e.preventDefault(); this.previousState = new Map(this.inkData); if (InkingControl.Instance.selectedTool !== InkTool.Eraser) { // start the new line, saves a uuid to represent the field of the stroke this._currentStrokeId = Utils.GenerateGuid(); const data = this.inkData; data.set(this._currentStrokeId, { pathData: [this.relativeCoordinatesForEvent(e.clientX, e.clientY)], color: InkingControl.Instance.selectedColor, width: InkingControl.Instance.selectedWidth, tool: InkingControl.Instance.selectedTool, page: NumCast(this.props.Document.curPage, -1) }); this.inkData = data; } } @action onPointerUp = (e: PointerEvent): void => { document.removeEventListener("pointermove", this.onPointerMove, true); document.removeEventListener("pointerup", this.onPointerUp, true); let coord = this.relativeCoordinatesForEvent(e.clientX, e.clientY); if (Math.abs(coord.x - this.inkMidX) > 500 || Math.abs(coord.y - this.inkMidY) > 500) { this.inkMidX = coord.x; this.inkMidY = coord.y; } e.stopPropagation(); e.preventDefault(); const batch = UndoManager.StartBatch("One ink stroke"); const oldState = this.previousState || new Map; this.previousState = undefined; const newState = new Map(this.inkData); UndoManager.AddEvent({ undo: () => this.inkData = oldState, redo: () => this.inkData = newState }); batch.end(); } @action onPointerMove = (e: PointerEvent): void => { e.stopPropagation(); e.preventDefault(); if (InkingControl.Instance.selectedTool !== InkTool.Eraser) { let data = this.inkData; // add points to new line as it is being drawn let strokeData = data.get(this._currentStrokeId); if (strokeData) { strokeData.pathData.push(this.relativeCoordinatesForEvent(e.clientX, e.clientY)); data.set(this._currentStrokeId, strokeData); } this.inkData = data; } } relativeCoordinatesForEvent = (ex: number, ey: number): { x: number, y: number } => { let [x, y] = this.props.getScreenTransform().transformPoint(ex, ey); return { x, y }; } @action removeLine = (id: string): void => { if (!this.previousState) { this.previousState = new Map(this.inkData); document.addEventListener("pointermove", this.onPointerMove, true); document.addEventListener("pointerup", this.onPointerUp, true); } let data = this.inkData; data.delete(id); this.inkData = data; } @computed get drawnPaths() { let curPage = NumCast(this.props.Document.curPage, -1); let paths = Array.from(this.inkData).reduce((paths, [id, strokeData]) => { if (strokeData.page === -1 || Math.round(strokeData.page) === Math.round(curPage)) { paths.push(); } return paths; }, [] as JSX.Element[]); return [ {paths.filter(path => path.props.tool !== InkTool.Highlighter)} , {paths.filter(path => path.props.tool === InkTool.Highlighter)} ]; } render() { let svgCanvasStyle = InkingControl.Instance.selectedTool !== InkTool.None ? "canSelect" : "noSelect"; return (
{this.props.children()} {this.drawnPaths}
); } }