diff options
-rw-r--r-- | src/client/views/InkControls.tsx | 12 | ||||
-rw-r--r-- | src/client/views/InkHandles.tsx | 1 | ||||
-rw-r--r-- | src/client/views/InkStrokeProperties.ts | 170 | ||||
-rw-r--r-- | src/fields/InkField.ts | 3 |
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(); |