aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/client/views/InkControls.tsx12
-rw-r--r--src/client/views/InkHandles.tsx1
-rw-r--r--src/client/views/InkStrokeProperties.ts170
-rw-r--r--src/fields/InkField.ts3
4 files changed, 121 insertions, 65 deletions
diff --git a/src/client/views/InkControls.tsx b/src/client/views/InkControls.tsx
index 4d8b2c6b5..da7b0df16 100644
--- a/src/client/views/InkControls.tsx
+++ b/src/client/views/InkControls.tsx
@@ -30,12 +30,18 @@ export class InkControls extends React.Component<InkControlProps> {
InkStrokeProperties.Instance.moveControl(0, 0, 1);
const controlUndo = UndoManager.StartBatch("DocDecs set radius");
const screenScale = this.props.ScreenToLocalTransform().Scale;
+ const order = controlIndex % 4;
+ const handleIndexA = order === 2 ? controlIndex - 1 : controlIndex - 2;
+ const handleIndexB = order === 2 ? controlIndex + 2 : controlIndex + 1;
setupMoveUpEvents(this, e,
(e: PointerEvent, down: number[], delta: number[]) => {
InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex);
return false;
},
- () => controlUndo?.end(), emptyFunction);
+ () => controlUndo?.end(), action((e: PointerEvent, doubleTap: boolean | undefined) =>
+ { if (doubleTap && InkStrokeProperties.Instance?._brokenIndices.includes(controlIndex)) {
+ InkStrokeProperties.Instance?.snapHandleTangent(controlIndex, handleIndexA, handleIndexB);
+ }}));
}
}
@@ -112,7 +118,9 @@ export class InkControls extends React.Component<InkControlProps> {
width={this._overControl === i ? strokeWidth * 1.5 : strokeWidth}
strokeWidth={strokeWidth / 6} stroke="#1F85DE"
fill={formatInstance?._currentPoint === control.I ? "#1F85DE" : "white"}
- onPointerDown={(e) => { this.changeCurrPoint(control.I); this.onControlDown(e, control.I); }}
+ onPointerDown={(e) => {
+ this.changeCurrPoint(control.I);
+ this.onControlDown(e, control.I); }}
onMouseEnter={() => this.onEnterControl(i)}
onMouseLeave={this.onLeaveControl}
pointerEvents="all"
diff --git a/src/client/views/InkHandles.tsx b/src/client/views/InkHandles.tsx
index a33380221..ba3fdf9db 100644
--- a/src/client/views/InkHandles.tsx
+++ b/src/client/views/InkHandles.tsx
@@ -24,7 +24,6 @@ export class InkHandles extends React.Component<InkControlProps> {
InkStrokeProperties.Instance.moveControl(0, 0, 1);
const controlUndo = UndoManager.StartBatch("DocDecs set radius");
const screenScale = this.props.ScreenToLocalTransform().Scale;
-
const order = handleIndex % 4;
const oppositeHandleIndex = order === 1 ? handleIndex - 3 : handleIndex + 3;
const controlIndex = order === 1 ? handleIndex - 1 : handleIndex + 2;
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 4ec03c560..a3f7562e0 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -71,70 +71,109 @@ export class InkStrokeProperties {
*/
@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 newPoint = { X: x, Y: y };
- 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 });
- }
-
- // Updating the indices of the control points whose handle tangency has been broken.
- this._brokenIndices = this._brokenIndices.map((control) => {
- if (control >= spNum) {
- return control + 4;
- } else {
- return control;
+ 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;
}
- });
-
- // const [handleA, handleB] = this.getNewHandlePoints(newPoint, pts[index-1], pts[index+1]);
- newPoints.push(newPoint);
- newPoints.push(newPoint);
- newPoints.push(newPoint);
- newPoints.push(newPoint);
-
- for (var i = spNum; i < ink.length; i++) {
- newPoints.push({ X: ink[i].X, Y: ink[i].Y });
-
}
- this._currentPoint = -1;
- Doc.GetProto(doc).data = new InkField(newPoints);
- }
+ });
}
}
- }));
+ 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 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);
+ 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) {
+ 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 });
+ }
+
+ }
+ // Updating the indices of the control points whose handle tangency has been broken.
+ this._brokenIndices = this._brokenIndices.map((control) => {
+ if (control >= newIndex) {
+ return control + 4;
+ } else {
+ return control;
+ }
+ });
+ this._currentPoint = -1;
+ return newPoints;
+ });
}
- getNewHandlePoints = (newControl: PointData, a: PointData, b: PointData) => {
- // find midpoint between the left and right control point of new control
- // rotate midpoint by +-pi/2 to get new handle points
- // multiplying x-y coordinates of both by 10/L where L is its current magnitude
- const angle = this.angleChange(a, b, newControl);
- const midpoint = this.rotatePoint(a, newControl, angle/2);
- // const handleA = this.rotatePoint(midpoint, newControl, -Math.PI/2);
- // const handleB = this.rotatePoint(midpoint, newControl, -Math.PI/2);
- const handleA = { X: midpoint.X + (20 * Math.cos(-Math.PI/2)), Y: midpoint.Y + (20 * Math.sin(-Math.PI/2)) };
- const handleB = { X: midpoint.X + (20 * Math.cos(Math.PI/2)), Y: midpoint.Y + (20 * Math.sin(Math.PI/2)) };
+ /**
+ * 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)));
+ 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 points of the current ink instance.
- * @returns The changed x- and y-coordinates of the control points.
+ * Deletes the current control point of the selected ink instance.
*/
@undoBatch
@action
@@ -160,9 +199,8 @@ export class InkStrokeProperties {
}, 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.
+ * Rotates the entire selected ink instance.
+ * @param angle The angle at which to rotate the ink in radians.
*/
@undoBatch
@action
@@ -210,6 +248,18 @@ export class InkStrokeProperties {
return newPoints;
})
+ snapHandleTangent = (controlIndex: number, handleIndexA: number, handleIndexB: number) => {
+ this.applyFunction((doc: Doc, ink: InkData) => {
+ this._brokenIndices.splice(this._brokenIndices.indexOf(controlIndex), 1);
+ 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;
+ });
+ }
+
/**
* Rotates the target point about the origin point for a given angle (radians).
*/
@@ -224,6 +274,11 @@ export class InkStrokeProperties {
return target;
}
+ /**
+ * Finds the angle 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);
@@ -255,20 +310,17 @@ export class InkStrokeProperties {
@action
moveHandle = (deltaX: number, deltaY: number, handleIndex: number, oppositeHandleIndex: number, controlIndex: number) =>
this.applyFunction((doc: Doc, ink: InkData, xScale: number, yScale: number) => {
- const order = handleIndex % 4;
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;
-
// Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle).
if (!this._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
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index 485376a34..1270a2dab 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -57,13 +57,10 @@ const strokeDataSchema = createSimpleSchema({
"*": true
});
-// Holistic class representing the store of an ink.
@Deserializable("ink")
export class InkField extends ObjectField {
@serializable(list(object(strokeDataSchema)))
readonly inkData: InkData;
- // inkData: InkData;
-
constructor(data: InkData) {
super();