diff options
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 192 | ||||
-rw-r--r-- | src/fields/InkField.ts | 9 |
2 files changed, 121 insertions, 80 deletions
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index ce4acb7ad..4aac49469 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -5,7 +5,7 @@ import { DateField } from "../../../../fields/DateField"; import { Doc, HeightSym, Opt, StrListCast, WidthSym } from "../../../../fields/Doc"; import { collectionSchema, documentSchema } from "../../../../fields/documentSchemas"; import { Id } from "../../../../fields/FieldSymbols"; -import { InkData, InkField, InkTool, PointData, Intersection } from "../../../../fields/InkField"; +import { InkData, InkField, InkTool, PointData, Intersection, Segment } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { ObjectField } from "../../../../fields/ObjectField"; import { RichTextField } from "../../../../fields/RichTextField"; @@ -53,6 +53,7 @@ import React = require("react"); import { ColorScheme } from "../../../util/SettingsManager"; import { Bezier } from "bezier-js"; import { GestureOverlay } from "../../GestureOverlay"; +import { constants } from "perf_hooks"; export const panZoomSchema = createSchema({ _panX: "number", @@ -648,6 +649,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return; if (CurrentUserUtils.SelectedTool !== InkTool.None) { this._currPoint = { X: e.clientX, Y: e.clientY }; + // Erasing ink strokes if intersections occur. const eraserIntersections: Intersection[] = this.getEraserIntersections(); if (eraserIntersections) this.eraseInkStrokes(eraserIntersections); } @@ -666,68 +668,28 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P } } - @action + /** + * Iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments, + * and deletes the original stroke. + * @param eraserIntersections The intersections made by the eraser. + */ eraseInkStrokes = (eraserIntersections: Intersection[]) => { eraserIntersections.forEach(intersect => { - var t1, t2; - var distT1: number, distT2: number; - distT1 = distT2 = Number.MAX_SAFE_INTEGER; - const t = intersect.t; const ink = intersect.ink; - const ctrlPoints = Cast(ink?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? []; - for (var i = 0; i < ctrlPoints.length - 3; i += 4) { - const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; - const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))); - const inkIntersections: number[] = this.getInkIntersections(curve); - inkIntersections.forEach(currentT => { - const diff = t - currentT; - if (diff > 0 && diff < distT1) { - distT1 = diff; - t1 = currentT; - } else if (diff < 0 && diff < distT2) { - distT2 = -diff; - t2 = currentT; - } - }); - - // Normal case --> deletion of entire stroke - if (!t1 && !t2) { - if (!this._deleteList.includes(ink)) { - this._deleteList.push(ink); - // Lowering ink opacity to give the user a visual indicator of deletion. - ink.Document.opacity = 0.5; - } - } else if (t1 && !t2) { - const splitCurve = curve.split(t1); - const leftInkData: PointData[] = splitCurve.left.points.map(p => ({ X: p.x, Y: p.y })); - GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, leftInkData); - if (!this._deleteList.includes(ink)) { - this._deleteList.push(ink); - // Lowering ink opacity to give the user a visual indicator of deletion. - ink.Document.opacity = 0.5; - } - } else if (!t1 && t2) { - const splitCurve = curve.split(t2); - const rightInkData: PointData[] = splitCurve.right.points.map(p => ({ X: p.x, Y: p.y })); - GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, rightInkData); - if (!this._deleteList.includes(ink)) { - this._deleteList.push(ink); - // Lowering ink opacity to give the user a visual indicator of deletion. - ink.Document.opacity = 0.5; - } - } else if (t1 && t2) { - const splitCurve1 = curve.split(t1); - const leftInkData: PointData[] = splitCurve1.left.points.map(p => ({ X: p.x, Y: p.y })); - GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, leftInkData); - const splitCurve2 = curve.split(t2); - const rightInkData: PointData[] = splitCurve2.right.points.map(p => ({ X: p.x, Y: p.y })); - GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, rightInkData); - - if (!this._deleteList.includes(ink)) { - this._deleteList.push(ink); - // Lowering ink opacity to give the user a visual indicator of deletion. - ink.Document.opacity = 0.5; - } + if (ink) { + if (!this._deleteList.includes(ink)) { + const segments: Segment[] = this.segmentInkStroke(ink, intersect.index ?? 0); + segments.forEach(segment => { + const newInkData: PointData[] = []; + // Appending all curves of the current segment together in order to render a single new stroke. + segment.forEach(curve => { + newInkData.push(...curve.points.map(p => ({ X: p.x, Y: p.y }))); + }); + GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, newInkData); + }); + this._deleteList.push(ink); + // Lowering ink opacity to give the user a visual indicator of deletion. + ink.Document.opacity = 0.5; } } }); @@ -756,9 +718,19 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y } }); if (inkView && intersects) { - intersects.forEach(t => { - intersections.push({ t: +t.toString(), ink: inkView }); - }); + for (const val of intersects) { + // Casting t-value from type: (string | number) to number for comparisons. + const t = +val.toString(); + var unique: boolean = true; + // Ensuring there are no duplicate intersections in the list returned. + for (const prevIntersect of intersections) { + if (prevIntersect.t === t) { + unique = false; + break; + } + } + if (unique) intersections.push({ t: +t.toString(), ink: inkView, curve: curve, index: i }); + } } } } @@ -766,36 +738,100 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P return intersections; } - getInkIntersections = (curve: Bezier): number[] => { - const intersections: number[] = []; - const selfIntersect = curve.selfintersects(); - if (selfIntersect) { - selfIntersect.forEach(t => { - intersections.push(+t.toString()); - }); + /** + * Performs segmentation of the ink stroke - creates "segments" or subsections of the current ink stroke at points in which the + * ink stroke intersects any other ink stroke (including itself). + * @param ink The ink DocumentView intersected by the eraser. + * @param excludeCurve The index of the curve in the ink document that the eraser intersection occurred. + * @returns The ink stroke represented as a list of segments, excluding the segment in which the eraser intersection occurred. + */ + @action + segmentInkStroke = (ink: DocumentView, excludeCurve: number): Segment[] => { + const segments: Segment[] = []; + var segment: Segment = []; + var excludeSegment: boolean = false; + const ctrlPoints = Cast(ink?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? []; + // Iterating through all of the curves of the intersected ink stroke. + for (var i = 0; i < ctrlPoints.length - 3; i += 4) { + // Performing "deletion" of a segment by excluding the segment in which intersection with the eraser occurred. + if (i === excludeCurve) excludeSegment = true; + const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; + const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))); + // Getting all t-value intersections of the current curve with all other curves. + const tVals: number[] = this.getInkIntersections(i, ink, curve); + if (tVals.length > 0) { + tVals.sort(); + const curveCopy = new Bezier(curve.points); + // Splitting only by the first t-value. + const pair = curveCopy.split(tVals[0]); + segment.push(pair.left); + if (!excludeSegment) { + segments.push(segment); + segment = [pair.right]; + excludeSegment = false; + } + // Splitting by the number of t-values returned. + // for (var index = 0; index < tVals.length; index++) { + // const curveCopy = new Bezier(curve.points); + // if (index === 0) { + // const pair = curveCopy.split(tVals[index]); + // segment.push(pair.left); + // segments.push(segment); + // segment = [pair.right]; + // } else if (index === tVals.length - 1) { + // segments.push(segment); + // const pair = curveCopy.split(tVals[index]); + // segment = [pair.right]; + // } else { + // segments.push(segment); + // const curve = curveCopy.split(index, index + 1); + // segment = [curve]; + // } + // } + } else { + segment.push(curve); + } } + if (!excludeSegment) segments.push(segment); + return segments; + } + /** + * Determines all possible intersections of the current curve of the intersected ink stroke with all other curves of all + * ink strokes in the current collection. + * @param i The index of the current curve within the inkData of the intersected ink stroke. + * @param ink The intersected DocumentView of the ink stroke. + * @param curve The current curve of the intersected ink stroke. + * @returns A list of all t-values at which intersections occur at the current curve of the intersected ink stroke. + */ + getInkIntersections = (i: number, ink: DocumentView, curve: Bezier): number[] => { + const tVals: number[] = []; + // Iterating through all ink strokes in the current freeform collection. this.childDocs .filter(doc => doc.type === DocumentType.INK) .forEach(doc => { const currInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView); const currCtrls = Cast(currInk?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? []; - for (var i = 0; i < currCtrls.length - 3; i += 4) { - const currArray = [currCtrls[i], currCtrls[i + 1], currCtrls[i + 2], currCtrls[i + 3]]; + for (var j = 0; j < currCtrls.length - 3; j += 4) { + const exclude = i === j || i === j - 4 || i === j + 4; + const sameDoc = ink?.Document === currInk?.Document; + // Ensuring that the curve intersected by the eraser is not checked for further ink intersections. + if (sameDoc && exclude) continue; + const currArray = [currCtrls[j], currCtrls[j + 1], currCtrls[j + 2], currCtrls[j + 3]]; const currCurve = new Bezier(currArray.map(p => ({ x: p.X, y: p.Y }))); const intersect = curve.intersects(currCurve); - if (intersect) { - intersect.forEach(t => { - intersections.push(+this.parseTValue(t.toString())); + if (intersect.length > 0) { + intersect.forEach(val => { + // Converting the Bezier.js Split type to a t-value number. + const t = +val.toString().split("/")[0]; + if (!tVals.includes(t)) tVals.push(t); }); } } }); - return intersections; + return tVals; } - parseTValue = (t: string) => t.split("/", 1)[0]; - handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => { if (!e.cancelBubble) { const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index 1d50b5e0d..f61313674 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -4,6 +4,7 @@ import { Deserializable } from "../client/util/SerializationHelper"; import { Copy, ToScriptString, ToString } from "./FieldSymbols"; import { ObjectField } from "./ObjectField"; import { DocumentView } from "../client/views/nodes/DocumentView"; +import { Bezier } from "bezier-js"; // Helps keep track of the current ink tool in use. export enum InkTool { @@ -22,10 +23,14 @@ export interface PointData { } export interface Intersection { - t: number; - ink: DocumentView; + t?: number; + ink?: DocumentView; + curve?: Bezier; + index?: number; } +export type Segment = Array<Bezier>; + // Defines an ink as an array of points. export type InkData = Array<PointData>; |