aboutsummaryrefslogtreecommitdiff
path: root/src/client/views/InkStrokeProperties.ts
diff options
context:
space:
mode:
authorgeireann <geireann.lindfield@gmail.com>2021-08-27 14:19:25 -0400
committergeireann <geireann.lindfield@gmail.com>2021-08-27 14:19:25 -0400
commitbe4fd2492ad706f30af28f33133a4df0e8049e12 (patch)
treee33b32f54be50122ed16c07d2b6f6b2e79239cb4 /src/client/views/InkStrokeProperties.ts
parentc5e96c72fcf149b9bcfe5f7f7a9c714de1d5fd9a (diff)
parent7c83bc30b3a6ed6061ef68bcef6a0e8941668b3c (diff)
Merge branch 'master' into schema-view-En-Hua
Diffstat (limited to 'src/client/views/InkStrokeProperties.ts')
-rw-r--r--src/client/views/InkStrokeProperties.ts380
1 files changed, 251 insertions, 129 deletions
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index b13b04f68..d527b2a05 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -1,124 +1,44 @@
import { action, computed, observable } from "mobx";
-import { ColorState } from 'react-color';
-import { Doc, Field, Opt } from "../../fields/Doc";
+import { Doc, DocListCast, Field, Opt } from "../../fields/Doc";
import { Document } from "../../fields/documentSchemas";
-import { InkField, InkData } from "../../fields/InkField";
+import { InkField, InkData, PointData, ControlPoint } from "../../fields/InkField";
+import { List } from "../../fields/List";
+import { listSpec } from "../../fields/Schema";
import { Cast, NumCast } from "../../fields/Types";
import { DocumentType } from "../documents/DocumentTypes";
import { SelectionManager } from "../util/SelectionManager";
import { undoBatch } from "../util/UndoManager";
-import { bool } from "sharp";
export class InkStrokeProperties {
static Instance: InkStrokeProperties | undefined;
- private _lastFill = "#D0021B";
- private _lastLine = "#D0021B";
- private _lastDash = "2";
- private _inkDocs: { x: number, y: number, width: number, height: number }[] = [];
-
@observable _lock = false;
- @observable _controlBtn = false;
- @observable _currPoint = -1;
+ @observable _controlButton = false;
+ @observable _currentPoint = -1;
- 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<string>);
+ constructor() {
+ InkStrokeProperties.Instance = this;
}
@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);
- });
- }
-
- constructor() {
- InkStrokeProperties.Instance = this;
- }
- @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);
- }
- }
- }
- }));
+ 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<string>);
}
+ /**
+ * 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.
+ */
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)) {
+ if (this.selectedInk?.length === 1 && (!requireCurrPoint || this._currentPoint !== -1)) {
const doc = Document(inkView.rootDoc);
if (doc.type === DocumentType.INK && doc.width && doc.height) {
const ink = Cast(doc.data, InkField)?.inkData;
@@ -145,17 +65,136 @@ export class InkStrokeProperties {
return appliedFunc;
}
+ /**
+ * Adds a new control point to the ink instance when editing its format.
+ * @param index The index of the new point.
+ * @param control The list of all control points of the ink.
+ */
+ @undoBatch
+ @action
+ addPoints = (x: number, y: number, points: InkData, index: number, controls: { X: number, Y: number }[]) => {
+ this.applyFunction((doc: Doc, ink: InkData) => {
+ const newControl = { X: x, Y: y };
+ const newPoints: InkData = [];
+ let [counter, start, end] = [0, 0, 0];
+ for (let k = 0; k < points.length; k++) {
+ if (end === 0) {
+ controls.forEach((control) => {
+ if (control.X === points[k].X && control.Y === points[k].Y) {
+ if (k < index) {
+ counter++;
+ start = k;
+ } else if (k > index) {
+ end = k;
+ }
+ }
+ });
+ }
+ }
+ if (end === 0) end = points.length - 1;
+ // Index of new control point with regards to the ink data.
+ const newIndex = Math.floor(counter / 2) * 4 + 2;
+ // Creating new ink data with the new control point and handle points inputted.
+ for (let i = 0; i < ink.length; i++) {
+ if (i === newIndex) {
+ const [handleA, handleB] = this.getNewHandlePoints(points.slice(start, index + 1), points.slice(index, end), newControl);
+ newPoints.push(handleA, newControl, newControl, handleB);
+ // Adjusting the magnitude of the left handle line of the right neighboring control point.
+ const [rightControl, rightHandle] = [points[end], ink[i]];
+ const scaledVector = this.getScaledHandlePoint(false, start, end, index, rightControl, rightHandle);
+ rightHandle && newPoints.push({ X: rightControl.X - scaledVector.X, Y: rightControl.Y - scaledVector.Y });
+ } else if (i === newIndex - 1) {
+ // Adjusting the magnitude of the right handle line of the left neighboring control point.
+ const [leftControl, leftHandle] = [points[start], ink[i]];
+ const scaledVector = this.getScaledHandlePoint(true, start, end, index, leftControl, leftHandle);
+ leftHandle && newPoints.push({ X: leftControl.X - scaledVector.X, Y: leftControl.Y - scaledVector.Y });
+ } else {
+ ink[i] && newPoints.push({ X: ink[i].X, Y: ink[i].Y });
+ }
+
+ }
+ let brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
+ // Updating the indices of the control points whose handle tangency has been broken.
+ if (brokenIndices) {
+ brokenIndices = new List(brokenIndices.map((control) => {
+ if (control >= newIndex) {
+ return control + 4;
+ } else {
+ return control;
+ }
+ }));
+ }
+ doc.brokenInkIndices = brokenIndices;
+ this._currentPoint = -1;
+ return newPoints;
+ });
+ }
+
+ /**
+ * Scales a handle point of a control point that is adjacent to a newly added one.
+ * @param isLeft Determines if the current control point is on the left or right side of the newly added one.
+ * @param start Beginning index of curve from the left control point to the newly added one.
+ * @param end Final index of curve from the newly added control point to its right neighbor.
+ */
+ getScaledHandlePoint(isLeft: boolean, start: number, end: number, index: number, control: PointData, handle: PointData) {
+ const prevSize = end - start;
+ const newSize = isLeft ? index - start : end - index;
+ const handleVector = { X: control.X - handle.X, Y: control.Y - handle.Y };
+ const scaledVector = { X: handleVector.X * (newSize / prevSize), Y: handleVector.Y * (newSize / prevSize) };
+ return scaledVector;
+ }
+
+ /**
+ * Determines the position of the handle points of a newly added control point by finding the
+ * tangent vectors to the split curve at the new control. Given the properties of Bézier curves,
+ * the tangent vector to a control point is equivalent to the first/last (depending on the direction
+ * of the curve) leg of the Bézier curve's derivative.
+ * (Source: https://pages.mtu.edu/~shene/COURSES/cs3621/NOTES/spline/Bezier/bezier-der.html)
+ *
+ * @param C The curve represented by all points from the previous control until the newly added point.
+ * @param D The curve represented by all points from the newly added point to the next control.
+ * @param newControl The newly added control point.
+ */
+ getNewHandlePoints = (C: PointData[], D: PointData[], newControl: PointData) => {
+ const [m, n] = [C.length, D.length];
+ let handleSizeA = Math.sqrt((Math.pow(newControl.X - C[0].X, 2)) + (Math.pow(newControl.Y - C[0].Y, 2)));
+ let handleSizeB = Math.sqrt((Math.pow(D[n - 1].X - newControl.X, 2)) + (Math.pow(D[n - 1].Y - newControl.Y, 2)));
+ // Scaling adjustments to improve the ratio between the magnitudes of the two handle lines.
+ // (Ensures that the new point added doesn't augment the inital shape of the curve much).
+ if (handleSizeA < 75 && handleSizeB < 75) {
+ handleSizeA *= 3;
+ handleSizeB *= 3;
+ }
+ if (Math.abs(handleSizeA - handleSizeB) < 50) {
+ handleSizeA *= 5;
+ handleSizeB *= 5;
+ } else if (Math.abs(handleSizeA - handleSizeB) < 150) {
+ handleSizeA *= 2;
+ handleSizeB *= 2;
+ }
+ // Finding the last leg of the derivative curve of C.
+ const dC = { X: (handleSizeA / n) * (C[m - 1].X - C[m - 2].X), Y: (handleSizeA / n) * (C[m - 1].Y - C[m - 2].Y) };
+ // Finding the first leg of the derivative curve of D.
+ const dD = { X: (handleSizeB / m) * (D[1].X - D[0].X), Y: (handleSizeB / m) * (D[1].Y - D[0].Y) };
+ const handleA = { X: newControl.X - dC.X, Y: newControl.Y - dC.Y };
+ const handleB = { X: newControl.X + dD.X, Y: newControl.Y + dD.Y };
+ return [handleA, handleB];
+ }
+
+ /**
+ * Deletes the current control point of the selected ink instance.
+ */
@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++) {
+ const newPoints: { X: number, Y: number }[] = [];
+ const toRemove = Math.floor(((this._currentPoint + 2) / 4));
+ for (let 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;
+ this._currentPoint = -1;
if (newPoints.length < 4) return undefined;
if (newPoints.length === 4) {
const newerPoints: { X: number, Y: number }[] = [];
@@ -166,12 +205,16 @@ export class InkStrokeProperties {
return newerPoints;
}
return newPoints;
- }, true);
+ }, true)
+ /**
+ * Rotates the entire selected ink instance.
+ * @param angle The angle at which to rotate the ink in radians.
+ */
@undoBatch
@action
- rotate = (angle: number) => {
- this.applyFunction((doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => {
+ rotateInk = (angle: number) => {
+ this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: 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 };
@@ -186,42 +229,121 @@ export class InkStrokeProperties {
});
}
+ /**
+ * Handles the movement/scaling of a control point.
+ */
@undoBatch
@action
- control = (xDiff: number, yDiff: number, controlNum: number) =>
- this.applyFunction((doc: Doc, ink: InkData, ptsXscale: number, ptsYscale: number) => {
+ 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 = controlNum % 4;
+ const order = controlIndex % 4;
for (var i = 0; i < ink.length; i++) {
- newPoints.push(
- (controlNum === i ||
- (order === 0 && i === controlNum + 1) ||
- (order === 0 && controlNum !== 0 && i === controlNum - 2) ||
- (order === 0 && controlNum !== 0 && i === controlNum - 1) ||
- (order === 3 && i === controlNum - 1) ||
- (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 1) ||
- (order === 3 && controlNum !== ink.length - 1 && i === controlNum + 2) ||
- ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlNum === 0 || controlNum === ink.length - 1))
- ) ?
- { X: ink[i].X - xDiff / ptsXscale, Y: ink[i].Y - yDiff / ptsYscale } :
- { X: ink[i].X, Y: ink[i].Y });
+ 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;
+ })
+
+ /**
+ * Snaps a control point with broken tangency back to synced rotation.
+ * @param handleIndexA The handle point that retains its current position.
+ * @param handleIndexB The handle point that is rotated to be 180 degrees from its opposite.
+ */
+ snapHandleTangent = (controlIndex: number, handleIndexA: number, handleIndexB: number) => {
+ this.applyFunction((doc: Doc, ink: InkData) => {
+ const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
+ if (brokenIndices) {
+ const newBrokenIndices = new List;
+ brokenIndices.forEach(brokenIndex => {
+ if (brokenIndex !== controlIndex) {
+ newBrokenIndices.push(brokenIndex);
+ }
+ });
+ doc.brokenInkIndices = newBrokenIndices;
+ const [controlPoint, handleA, handleB] = [ink[controlIndex], ink[handleIndexA], ink[handleIndexB]];
+ const oppositeHandleA = this.rotatePoint(handleA, controlPoint, Math.PI);
+ const angleDifference = this.angleChange(handleB, oppositeHandleA, controlPoint);
+ const newHandleB = this.rotatePoint(handleB, controlPoint, angleDifference);
+ ink[handleIndexB] = newHandleB;
+ return ink;
+ }
});
+ }
- @undoBatch
+ /**
+ * Rotates the target point about the origin point for a given angle (radians).
+ */
@action
- switchStk = (color: ColorState) => {
- const val = String(color.hex);
- this.colorStk = val;
- return true;
+ rotatePoint = (target: PointData, origin: PointData, angle: number) => {
+ const rotatedTarget = { X: target.X - origin.X, Y: target.Y - origin.Y };
+ const newX = Math.cos(angle) * rotatedTarget.X - Math.sin(angle) * rotatedTarget.Y;
+ const newY = Math.sin(angle) * rotatedTarget.X + Math.cos(angle) * rotatedTarget.Y;
+ rotatedTarget.X = newX + origin.X;
+ rotatedTarget.Y = newY + origin.Y;
+ return rotatedTarget;
+ }
+
+ /**
+ * Finds the angle (in radians) between two inputted vectors.
+ *
+ * α = arccos(a·b / |a|·|b|), where a and b are both vectors.
+ */
+ angleBetweenTwoVectors = (vectorA: PointData, vectorB: PointData) => {
+ 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;
+ return Math.acos(dotProduct);
}
+ /**
+ * 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.
+ const vectorA = { X: a.X - origin.X, Y: a.Y - origin.Y };
+ const 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 theta = this.angleBetweenTwoVectors(vectorA, vectorB);
+ return sign * theta;
+ }
+
+ /**
+ * Handles the movement/scaling of a handle point.
+ */
@undoBatch
@action
- switchFil = (color: ColorState) => {
- const val = String(color.hex);
- this.colorFil = val;
- return true;
- }
+ moveHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
+ this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => {
+ const oldHandlePoint = ink[handleIndex];
+ let oppositeHandlePoint = ink[oppositeHandleIndex];
+ const controlPoint = ink[controlIndex];
+ const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale };
+ ink[handleIndex] = newHandlePoint;
+ const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number"));
+ // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
+ if ((!brokenIndices || !brokenIndices?.includes(controlIndex)) && handleIndex !== 1 && handleIndex !== ink.length - 2) {
+ const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint);
+ oppositeHandlePoint = this.rotatePoint(oppositeHandlePoint, controlPoint, angle);
+ ink[oppositeHandleIndex] = oppositeHandlePoint;
+ }
+ return ink;
+ })
} \ No newline at end of file