import { action, computed, observable } from "mobx"; import { ColorState } from 'react-color'; import { Doc, Field, Opt } from "../../fields/Doc"; import { Document } from "../../fields/documentSchemas"; import { InkField, InkData, PointData } from "../../fields/InkField"; import { Cast, NumCast } from "../../fields/Types"; import { DocumentType } from "../documents/DocumentTypes"; import { SelectionManager } from "../util/SelectionManager"; import { undoBatch } from "../util/UndoManager"; export class InkStrokeProperties { static Instance: InkStrokeProperties | undefined; private _lastFill = "#D0021B"; private _lastLine = "#D0021B"; private _lastDash = "2"; @observable _lock = false; @observable _controlBtn = false; @observable _currPoint = -1; constructor() { InkStrokeProperties.Instance = this; } getField(key: string) { return this.selectedInk?.reduce((p, i) => (p === undefined || (p && p === i.rootDoc[key])) && i.rootDoc[key] !== "0" ? Field.toString(i.rootDoc[key] as Field) : "", undefined as Opt); } @computed get selectedInk() { const inks = SelectionManager.Views().filter(i => Document(i.rootDoc).type === DocumentType.INK); return inks.length ? inks : undefined; } @computed get unFilled() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.fillColor ? true : false, true) || false; } @computed get unStrokd() { return this.selectedInk?.reduce((p, i) => p && !i.rootDoc.color ? true : false, true) || false; } @computed get solidFil() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.fillColor ? true : false, true) || false; } @computed get solidStk() { return this.selectedInk?.reduce((p, i) => p && i.rootDoc.color && (!i.rootDoc.strokeDash || i.rootDoc.strokeDash === "0") ? true : false, true) || false; } @computed get dashdStk() { return !this.unStrokd && this.getField("strokeDash") || ""; } @computed get colorFil() { const ccol = this.getField("fillColor") || ""; ccol && (this._lastFill = ccol); return ccol; } @computed get colorStk() { const ccol = this.getField("color") || ""; ccol && (this._lastLine = ccol); return ccol; } @computed get widthStk() { return this.getField("strokeWidth") || "1"; } @computed get markHead() { return this.getField("strokeStartMarker") || ""; } @computed get markTail() { return this.getField("strokeEndMarker") || ""; } @computed get shapeHgt() { return this.getField("_height"); } @computed get shapeWid() { return this.getField("_width"); } @computed get shapeXps() { return this.getField("x"); } @computed get shapeYps() { return this.getField("y"); } @computed get shapeRot() { return this.getField("rotation"); } set unFilled(value) { this.colorFil = value ? "" : this._lastFill; } set solidFil(value) { this.unFilled = !value; } set colorFil(value) { value && (this._lastFill = value); this.selectedInk?.forEach(i => i.rootDoc.fillColor = value ? value : undefined); } set colorStk(value) { value && (this._lastLine = value); this.selectedInk?.forEach(i => i.rootDoc.color = value ? value : undefined); } set markHead(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeStartMarker = value); } set markTail(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeEndMarker = value); } set unStrokd(value) { this.colorStk = value ? "" : this._lastLine; } set solidStk(value) { this.dashdStk = ""; this.unStrokd = !value; } set dashdStk(value) { value && (this._lastDash = value) && (this.unStrokd = false); this.selectedInk?.forEach(i => i.rootDoc.strokeDash = value ? this._lastDash : undefined); } set shapeXps(value) { this.selectedInk?.forEach(i => i.rootDoc.x = Number(value)); } set shapeYps(value) { this.selectedInk?.forEach(i => i.rootDoc.y = Number(value)); } set shapeRot(value) { this.selectedInk?.forEach(i => i.rootDoc.rotation = Number(value)); } set widthStk(value) { this.selectedInk?.forEach(i => i.rootDoc.strokeWidth = Number(value)); } set shapeWid(value) { this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { const oldWidth = NumCast(i.rootDoc._width); i.rootDoc._width = Number(value); this._lock && (i.rootDoc._height = (i.rootDoc._width * NumCast(i.rootDoc._height)) / oldWidth); }); } set shapeHgt(value) { this.selectedInk?.filter(i => i.rootDoc._width && i.rootDoc._height).forEach(i => { const oldHeight = NumCast(i.rootDoc._height); i.rootDoc._height = Number(value); this._lock && (i.rootDoc._width = (i.rootDoc._height * NumCast(i.rootDoc._width)) / oldHeight); }); } /** * Adds a new control point to the ink instance when editing its format. * @param x The x-coordinate of the current new point. * @param y The y-coordinate of the current new point. * @param pts The list containing all of the points to be added in PointData form. * @param index The index of the current new point. * @param control The list of all control points of the ink. */ @undoBatch @action addPoints = (x: number, y: number, pts: { X: number, Y: number }[], index: number, control: { X: number, Y: number }[]) => { this.selectedInk?.forEach(action(inkView => { if (this.selectedInk?.length === 1) { const doc = Document(inkView.rootDoc); if (doc.type === DocumentType.INK) { const ink = Cast(doc.data, InkField)?.inkData; if (ink) { const newPoints: { X: number, Y: number }[] = []; var counter = 0; for (var k = 0; k < index; k++) { control.forEach(pt => (pts[k].X === pt.X && pts[k].Y === pt.Y) && counter++); } //decide where to put the new coordinate const spNum = Math.floor(counter / 2) * 4 + 2; for (var i = 0; i < spNum; i++) { ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y }); } for (var j = 0; j < 4; j++) { newPoints.push({ X: x, Y: y }); } for (var i = spNum; i < ink.length; i++) { newPoints.push({ X: ink[i].X, Y: ink[i].Y }); } this._currPoint = -1; Doc.GetProto(doc).data = new InkField(newPoints); } } } })); } /** * Helper function that enables other functions to be applied to a particular ink instance. * @param func The inputted function. * @param requireCurrPoint Indicates whether the current selected point is needed. * @returns The applied function. */ applyFunction = (func: (doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { X: number, Y: number }[] | undefined, requireCurrPoint: boolean = false) => { var appliedFunc = false; this.selectedInk?.forEach(action(inkView => { if (this.selectedInk?.length === 1 && (!requireCurrPoint || this._currPoint !== -1)) { const doc = Document(inkView.rootDoc); if (doc.type === DocumentType.INK && doc.width && doc.height) { const ink = Cast(doc.data, InkField)?.inkData; if (ink) { const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X)); const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); const ptsXscale = NumCast(doc._width) / (oldXrange.max - oldXrange.min); const ptsYscale = NumCast(doc._height) / (oldYrange.max - oldYrange.min); const newPoints = func(doc, ink, ptsXscale, ptsYscale); if (newPoints) { const newXrange = (xs => ({ min: Math.min(...xs), max: Math.max(...xs) }))(newPoints.map(p => p.X)); const newYrange = (ys => ({ min: Math.min(...ys), max: Math.max(...ys) }))(newPoints.map(p => p.Y)); doc._width = (newXrange.max - newXrange.min) * ptsXscale; doc._height = (newYrange.max - newYrange.min) * ptsYscale; doc.x = (oldXrange.coord + (newXrange.min - oldXrange.min) * ptsXscale); doc.y = (oldYrange.coord + (newYrange.min - oldYrange.min) * ptsYscale); Doc.GetProto(doc).data = new InkField(newPoints); appliedFunc = true; } } } } })); return appliedFunc; } /** * Deletes the points of the current ink instance. * @returns The changed x- and y-coordinates of the control points. */ @undoBatch @action deletePoints = () => this.applyFunction((doc: Doc, ink: InkData) => { var newPoints: { X: number, Y: number }[] = []; const toRemove = Math.floor(((this._currPoint + 2) / 4)); for (var i = 0; i < ink.length; i++) { if (Math.floor((i + 2) / 4) !== toRemove && (toRemove !== 0 || i > 3)) { newPoints.push({ X: ink[i].X, Y: ink[i].Y }); } } this._currPoint = -1; if (newPoints.length < 4) return undefined; if (newPoints.length === 4) { const newerPoints: { X: number, Y: number }[] = []; newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); newerPoints.push({ X: newPoints[0].X, Y: newPoints[0].Y }); newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); newerPoints.push({ X: newPoints[3].X, Y: newPoints[3].Y }); return newerPoints; } return newPoints; }, true); /** * Rotates the points of the current ink instance by a certain angle degree. * @param angle The angle at which to rotate the ink (all of its x- and y-coordinates). * @returns The changed x- and y-coordinates of the control points. */ @undoBatch @action rotate = (angle: number) => { this.applyFunction((doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => { const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X)); const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); const centerPoint = { X: (oldXrange.min + oldXrange.max) / 2, Y: (oldYrange.min + oldYrange.max) / 2 }; const newPoints: { X: number, Y: number }[] = []; ink.map(i => ({ X: i.X - centerPoint.X, Y: i.Y - centerPoint.Y })).forEach(i => { const newX = Math.cos(angle) * i.X - Math.sin(angle) * i.Y; const newY = Math.sin(angle) * i.X + Math.cos(angle) * i.Y; newPoints.push({ X: newX + centerPoint.X, Y: newY + centerPoint.Y }); }); doc.rotation = NumCast(doc.rotation) + angle; return newPoints; }); } /** * Handles the movement/scaling of a control point. */ @undoBatch @action moveControl = (deltaX: number, deltaY: number, controlIndex: number) => this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { const newPoints: { X: number, Y: number }[] = []; const order = controlIndex % 4; for (var i = 0; i < ink.length; i++) { const leftHandlePoint = order === 0 && i === controlIndex + 1; const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2; if (controlIndex === i || leftHandlePoint || rightHandlePoint || (order === 0 && controlIndex !== 0 && i === controlIndex - 1) || (order === 3 && i === controlIndex - 1) || (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) || (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) || ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))) { newPoints.push({ X: ink[i].X - deltaX / xScale, Y: ink[i].Y - deltaY / yScale }); } else { newPoints.push({ X: ink[i].X, Y: ink[i].Y }); } } return newPoints; }); /** * Rotates the target point about the origin point for a given angle (radians). */ @action rotatePoint = (target: PointData, origin: PointData, angle: number) => { target.X -= origin.X; target.Y -= origin.Y; const newX = Math.cos(angle) * target.X - Math.sin(angle) * target.Y; const newY = Math.sin(angle) * target.X + Math.cos(angle) * target.Y; target.X = newX + origin.X; target.Y = newY + origin.Y; return target } /** * Finds the angle difference (in radians) between two vectors relative to an arbitrary origin. */ angleChange = (a: PointData, b: PointData, origin: PointData) => { // Finding vector representation of inputted points relative to new origin. let vectorA = { X: a.X - origin.X, Y: a.Y - origin.Y }; let vectorB = { X: b.X - origin.X, Y: b.Y - origin.Y }; const crossProduct = vectorB.X * vectorA.Y - vectorB.Y * vectorA.X; // Determining whether rotation is clockwise or counterclockwise. const sign = crossProduct < 0 ? 1 : -1; const magnitudeA = Math.sqrt(vectorA.X * vectorA.X + vectorA.Y * vectorA.Y); const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y); // Normalizing the vectors. vectorA = { X: vectorA.X / magnitudeA, Y: vectorA.Y / magnitudeA }; vectorB = { X: vectorB.X / magnitudeB, Y: vectorB.Y / magnitudeB }; const dotProduct = vectorB.X * vectorA.X + vectorB.Y * vectorA.Y; const theta = Math.acos(dotProduct); return sign * theta; } /** * Handles the movement/scaling of a handle point. */ @undoBatch @action moveHandle = (deltaX: number, deltaY: number, handleIndex: number) => this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => { const newPoints: { X: number, Y: number }[] = []; const order = handleIndex % 4; let newHandlePoint = { X: 0, Y: 0 }; for (var i = 0; i < ink.length; i++) { if (handleIndex === i) { newHandlePoint = { X: ink[i].X - deltaX / xScale, Y: ink[i].Y - deltaY / yScale }; newPoints.push({ X: newHandlePoint.X, Y: newHandlePoint.Y }); } else { newPoints.push({ X: ink[i].X, Y: ink[i].Y }); } } if (handleIndex !== 1 && handleIndex !== ink.length - 2) { const oldHandlePoint = ink[handleIndex]; let oppositeHandlePoint = order === 1 ? ink[handleIndex - 3] : ink[handleIndex + 3]; const controlPoint = order === 1 ? ink[handleIndex - 1] : ink[handleIndex + 1]; const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); order === 1 ? newPoints[handleIndex - 3] = oppositeHandlePoint : newPoints[handleIndex + 3] = oppositeHandlePoint; } return newPoints; }); /** * Changes the color of the border of the ink instance. * @param color The new hex value to change the border to. * @returns true. */ @undoBatch @action switchStk = (color: ColorState) => { const val = String(color.hex); this.colorStk = val; return true; } /** * Changes the color of the fill of the ink instance. * @param color The new hex value to change the fill to. * @returns true. */ @undoBatch @action switchFil = (color: ColorState) => { const val = String(color.hex); this.colorFil = val; return true; } }