diff options
author | bobzel <zzzman@gmail.com> | 2024-04-24 00:36:30 -0400 |
---|---|---|
committer | bobzel <zzzman@gmail.com> | 2024-04-24 00:36:30 -0400 |
commit | 6ddfdb20e6a0b2c896594ad2c20292c6cfea0c1a (patch) | |
tree | 1cfda249aa5bd901f87278cad869bd5e8827da71 | |
parent | 0c33bc8033c9877abbe6e4074a687559bc4948d0 (diff) |
several fixes to radius ink eraser to fix eraser shap, intersections with straight lines
-rw-r--r-- | src/Utils.ts | 4 | ||||
-rw-r--r-- | src/client/views/InkingStroke.tsx | 129 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 91 |
3 files changed, 105 insertions, 119 deletions
diff --git a/src/Utils.ts b/src/Utils.ts index 291d7c799..226051f7d 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -348,6 +348,10 @@ export namespace Utils { return project(near.point.x, near.point.y, fres[1][0], fres[1][1], fres[1][2], fres[1][3]); } + export function ptDistance(p1: { x: number; y: number }, p2: { x: number; y: number }) { + return Math.sqrt((p2.x - p1.x) ** 2 + (p2.y - p1.y) ** 2); + } + export function getNearestPointInPerimeter(l: number, t: number, w: number, h: number, x: number, y: number) { const r = l + w, b = t + h; diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 1ae96edfd..03acd5393 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -331,87 +331,54 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() ); }; - coordWithSlope = (distance: number, slope: number) => { - return Math.sqrt((distance * distance) / (1 + slope * slope)) - } - - - splitByEraser = (startInkCoords: {X: number; Y: number}, inkCoords: {X: number; Y: number}) => { - + splitByEraser = (startInkCoordsIn: { X: number; Y: number }, endInkCoordsIn: { X: number; Y: number }) => { const radius = ActiveEraserWidth() / 2 + 3; // reduce values to avoid extreme radii - console.log("radius", radius); - const ctrlPtDist = 0.552284749831 * radius; // (4/3) * tan(pi / 8) * radius - - var perpSlope = (inkCoords.Y - startInkCoords.Y) / (inkCoords.X - startInkCoords.X); - if (perpSlope > 500) { - perpSlope = 500; // avoid ridiculously high/infinity slopes - } else if (perpSlope < -500) { - perpSlope = -500 - } else if (perpSlope === 0) { - perpSlope = 0.002 - } - const slope = - 1 / perpSlope - console.log("slope", slope); - var points: { X: number; Y: number }[] = [] - if (startInkCoords.X > inkCoords.X) { - points = [ - { X: startInkCoords.X, Y: startInkCoords.Y }, - { X: startInkCoords.X, Y: startInkCoords.Y }, - // { X: startInkCoords.X + this.coordWithSlope(ctrlPtDist, slope), Y: startInkCoords.Y + slope * this.coordWithSlope(ctrlPtDist, slope)}, - - { X: inkCoords.X + this.coordWithSlope(radius, slope) + this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) }, - { X: inkCoords.X + this.coordWithSlope(radius, slope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) }, - { X: inkCoords.X + this.coordWithSlope(radius, slope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) }, - { X: inkCoords.X + this.coordWithSlope(radius, slope) - this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) }, - - // { X: inkCoords.X - this.coordWithSlope(radius, perpSlope) + this.coordWithSlope(ctrlPtDist, slope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) }, - { X: inkCoords.X - this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) }, - { X: inkCoords.X - this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) }, - { X: inkCoords.X - this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) }, - { X: inkCoords.X - this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) }, - // { X: inkCoords.X - this.coordWithSlope(radius, perpSlope) - this.coordWithSlope(ctrlPtDist, slope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) }, - - { X: inkCoords.X - this.coordWithSlope(radius, slope) - this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) }, - { X: inkCoords.X - this.coordWithSlope(radius, slope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) }, - { X: inkCoords.X - this.coordWithSlope(radius, slope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) }, - { X: inkCoords.X - this.coordWithSlope(radius, slope) + this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) }, - - // { X: startInkCoords.X - this.coordWithSlope(ctrlPtDist, slope), Y: startInkCoords.Y - slope * this.coordWithSlope(ctrlPtDist, slope) }, - { X: startInkCoords.X, Y: startInkCoords.Y }, - { X: startInkCoords.X, Y: startInkCoords.Y }, - ]; - } else { - points = [ - { X: startInkCoords.X, Y: startInkCoords.Y }, - { X: startInkCoords.X, Y: startInkCoords.Y }, - // { X: startInkCoords.X + this.coordWithSlope(ctrlPtDist, slope), Y: startInkCoords.Y + slope * this.coordWithSlope(ctrlPtDist, slope)}, - - { X: inkCoords.X + this.coordWithSlope(radius, slope) - this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) }, - { X: inkCoords.X + this.coordWithSlope(radius, slope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) }, - { X: inkCoords.X + this.coordWithSlope(radius, slope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) }, - { X: inkCoords.X + this.coordWithSlope(radius, slope) + this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y + slope * this.coordWithSlope(radius, slope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) }, - - // { X: inkCoords.X - this.coordWithSlope(radius, perpSlope) + this.coordWithSlope(ctrlPtDist, slope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) }, - { X: inkCoords.X + this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y + perpSlope * this.coordWithSlope(radius, perpSlope) }, - { X: inkCoords.X + this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y + perpSlope * this.coordWithSlope(radius, perpSlope) }, - { X: inkCoords.X + this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y + perpSlope * this.coordWithSlope(radius, perpSlope) }, - { X: inkCoords.X + this.coordWithSlope(radius, perpSlope), Y: inkCoords.Y + perpSlope * this.coordWithSlope(radius, perpSlope) }, - // { X: inkCoords.X - this.coordWithSlope(radius, perpSlope) - this.coordWithSlope(ctrlPtDist, slope), Y: inkCoords.Y - perpSlope * this.coordWithSlope(radius, perpSlope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) }, - - { X: inkCoords.X - this.coordWithSlope(radius, slope) + this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) + perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) }, - { X: inkCoords.X - this.coordWithSlope(radius, slope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) }, - { X: inkCoords.X - this.coordWithSlope(radius, slope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) }, - { X: inkCoords.X - this.coordWithSlope(radius, slope) - this.coordWithSlope(ctrlPtDist, perpSlope), Y: inkCoords.Y - slope * this.coordWithSlope(radius, slope) - perpSlope * this.coordWithSlope(ctrlPtDist, perpSlope) }, - - // { X: startInkCoords.X - this.coordWithSlope(ctrlPtDist, slope), Y: startInkCoords.Y - slope * this.coordWithSlope(ctrlPtDist, slope) }, - { X: startInkCoords.X, Y: startInkCoords.Y }, - { X: startInkCoords.X, Y: startInkCoords.Y }, - ]; - } - - const eraserCircle = new InkField(points); - return eraserCircle; - } + const c = 0.551915024494; // circle tangent length to side ratio + const movement = { x: endInkCoordsIn.X - startInkCoordsIn.X, y: endInkCoordsIn.Y - startInkCoordsIn.Y }; + const moveLen = Math.sqrt(movement.x ** 2 + movement.y ** 2); + const direction = { x: (movement.x / moveLen) * radius, y: (movement.y / moveLen) * radius }; + const normal = { x: -direction.y, y: direction.x }; // prettier-ignore + + const startCoords = { X: startInkCoordsIn.X - direction.x, Y: startInkCoordsIn.Y - direction.y }; + const endCoords = { X: endInkCoordsIn.X + direction.x, Y: endInkCoordsIn.Y + direction.y }; + return new InkField([ + // left bot arc + { X: startCoords.X, Y: startCoords.Y }, // prettier-ignore + { X: startCoords.X + normal.x * c, Y: startCoords.Y + normal.y * c }, // prettier-ignore + { X: startCoords.X + direction.x + normal.x - direction.x * c, Y: startCoords.Y + direction.y + normal.y - direction.y * c }, + { X: startCoords.X + direction.x + normal.x, Y: startCoords.Y + direction.y + normal.y }, // prettier-ignore + + // bot + { X: startCoords.X + direction.x + normal.x, Y: startCoords.Y + direction.y + normal.y }, // prettier-ignore + { X: startCoords.X + direction.x + normal.x + direction.x * c, Y: startCoords.Y + direction.y + normal.y + direction.y * c }, + { X: endCoords.X - direction.x + normal.x - direction.x * c, Y: endCoords.Y - direction.y + normal.y - direction.y * c }, // prettier-ignore + { X: endCoords.X - direction.x + normal.x, Y: endCoords.Y - direction.y + normal.y }, // prettier-ignore + + // right bot arc + { X: endCoords.X - direction.x + normal.x, Y: endCoords.Y - direction.y + normal.y }, // prettier-ignore + { X: endCoords.X - direction.x + normal.x + direction.x * c, Y: endCoords.Y - direction.y + normal.y + direction.y * c}, // prettier-ignore + { X: endCoords.X + normal.x * c, Y: endCoords.Y + normal.y * c }, // prettier-ignore + { X: endCoords.X, Y: endCoords.Y }, // prettier-ignore + + // right top arc + { X: endCoords.X, Y: endCoords.Y }, // prettier-ignore + { X: endCoords.X - normal.x * c, Y: endCoords.Y - normal.y * c }, // prettier-ignore + { X: endCoords.X - direction.x - normal.x + direction.x * c, Y: endCoords.Y - direction.y - normal.y + direction.y * c}, // prettier-ignore + { X: endCoords.X - direction.x - normal.x, Y: endCoords.Y - direction.y - normal.y }, // prettier-ignore + + // top + { X: endCoords.X - direction.x - normal.x, Y: endCoords.Y - direction.y - normal.y }, // prettier-ignore + { X: endCoords.X - direction.x - normal.x - direction.x * c, Y: endCoords.Y - direction.y - normal.y - direction.y * c}, // prettier-ignore + { X: startCoords.X + direction.x - normal.x + direction.x * c, Y: startCoords.Y + direction.y - normal.y + direction.y * c }, + { X: startCoords.X + direction.x - normal.x, Y: startCoords.Y + direction.y - normal.y }, // prettier-ignore + + // left top arc + { X: startCoords.X + direction.x - normal.x, Y: startCoords.Y + direction.y - normal.y }, // prettier-ignore + { X: startCoords.X + direction.x - normal.x - direction.x * c, Y: startCoords.Y + direction.y - normal.y - direction.y * c }, // prettier-ignore + { X: startCoords.X - normal.x * c, Y: startCoords.Y - normal.y * c }, // prettier-ignore + { X: startCoords.X, Y: startCoords.Y }, // prettier-ignore + ]); + }; _subContentView: ViewBoxInterface | undefined; setSubContentView = (doc: ViewBoxInterface) => (this._subContentView = doc); @@ -627,4 +594,4 @@ export function ActiveInkBezierApprox(): string { } export function ActiveEraserWidth(): number { return Number(ActiveInkPen()?.eraserWidth); -}
\ No newline at end of file +} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 78dae87c3..9df9c5492 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -620,6 +620,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection case InkTool.SegmentEraser: case InkTool.RadiusEraser: this._batch = UndoManager.StartBatch('collectionErase'); + this._eraserPts.length = 0; setupMoveUpEvents(this, e, this.onEraserMove, this.onEraserUp, emptyFunction); break; case InkTool.None: @@ -728,6 +729,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; _eraserLock = 0; + _eraserPts: number[][] = []; // keep track of the last few eraserPts to make the eraser circle 'stretch' /** * Erases strokes by intersecting them with an invisible "eraser stroke". * By default this iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments, @@ -737,6 +739,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => { const currPoint = { X: e.clientX, Y: e.clientY }; + this._eraserPts.push([currPoint.X, currPoint.Y]); + this._eraserPts = this._eraserPts.slice(Math.max(0, this._eraserPts.length - 5)); if (this._eraserLock) return false; // bcz: should be fixed by putting it on a queue to be processed after the last eraser movement is processed. this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => { if (!this._deleteList.includes(intersect.inkView)) { @@ -749,7 +753,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const segments = Doc.ActiveTool === InkTool.SegmentEraser ? this.segmentErase(intersect.inkView, intersect.t) // intersect.t is where the eraser intersected the ink stroke - want to remove the segment that starts at the intersection just before this t value and goes to the one just after it - : this.radiusErase(intersect.inkView, { X: down[0], Y: down[1] }, currPoint); + : this.radiusErase(intersect.inkView, { X: this._eraserPts[0][0], Y: this._eraserPts[0][1] }, currPoint); segments?.forEach(segment => this.forceStrokeGesture( e, @@ -832,16 +836,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const segments: Segment[] = []; const startInkCoords = ink.ComponentView?.ptFromScreen?.(startPt); const inkCoords = ink.ComponentView?.ptFromScreen?.(screenEraserPt); // coordinates in ink space - if (!inkCoords) return []; + if (!inkCoords || !startInkCoords) return []; var eraseSegment: Segment = []; // for eraser visualization const inkStroke = ink?.ComponentView as InkingStroke; const eraserStroke: InkData = inkStroke.splitByEraser(startInkCoords, inkCoords).inkData; const strokeToTVals: Map<InkingStroke, number[]> = new Map<InkingStroke, number[]>(); - for (var i = 0; i < eraserStroke.length - 3; i +=4) { + for (var i = 0; i < eraserStroke.length - 3; i += 4) { eraseSegment.push(InkField.Segment(eraserStroke, i)); // for eraser visualization - this.getOtherInkIntersections(i, eraserStroke, strokeToTVals); + this.getOtherInkIntersections(ink, i, eraserStroke, strokeToTVals); } strokeToTVals.forEach((tVals, inkStroke) => { var segment1: Segment = []; @@ -852,7 +856,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection var hasSplit = false; var continueErasing = false; - + // below is curve splitting logic if (tVals.length) { for (var i = 0; i < inkData.length - 3; i += 4) { @@ -881,12 +885,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } else if (tVals.length === 1) { if (tVals[0] > currCurveT && tVals[0] < currCurveT + 1) { - if (tVals[0] < Math.floor(inkData.length / 4) - tVals[0]) { + // this heuristic for determine which segment to keep is not quite right even though it will work most of the time. + // We should really store the eraser intersection normal in getOtherInkIntersections, + // and then test its dot product with the tangent of the stroke at the intersection point. + // if the dot product is positive, then erase the first part of the stroke, otherwise the second. + const leftDist = Utils.ptDistance({ x: inkCoords.X, y: inkCoords.Y }, inkSegment.points.lastElement()); + const rightDist = Utils.ptDistance({ x: inkCoords.X, y: inkCoords.Y }, inkSegment.points[0]); + const splits = inkSegment.split(tVals[0] - currCurveT); + if (leftDist < rightDist) { // if it's on the first end - segment1.push(inkSegment.split(tVals[0] - currCurveT, 1)); + segment1.push(splits.left); hasSplit = true; } else { - segment1.push(inkSegment.split(0, tVals[0] - currCurveT)); + segment1.push(splits.right); hasSplit = true; } } else { @@ -1084,6 +1095,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // for some reason bezier.js doesn't handle the case of intersecting a linear curve, so we wrap the intersection // call in a test for linearity bintersects = (curve: Bezier, otherCurve: Bezier) => { + if ((curve as any)._linear) { + // bezier.js doesn't intersect properly if the curve is actually a line -- so get intersect other curve against this line, then figure out the t coordinates of the intersection on this line + const intersections = otherCurve.lineIntersects({ p1: curve.points[0], p2: curve.points[3] }); + if (intersections.length) { + const intPt = otherCurve.get(intersections[0]); + const intT = curve.project(intPt).t; + return intT ? [intT] : []; + } + } if ((otherCurve as any)._linear) { return curve.lineIntersects({ p1: otherCurve.points[0], p2: otherCurve.points[3] }); } @@ -1135,43 +1155,38 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; /** - * For radius erase, a function to find an eraser's intersections with all ink strokes on the freeform canvas. + * For radius erase, a function to find an eraser's intersections with a single ink stroke + * @param otherInkDocVIew the ink document that is being erased * @param i the index of the eraser's segment * @param points the eraser's ink data points * @param strokeToTVals Map of InkingStroke to the tVals of its intersections with the eraser - * @returns + * @returns */ - getOtherInkIntersections = (i: number, points: InkData, strokeToTVals: Map<InkingStroke, number[]>): Map<InkingStroke, number[]> => { - // const tVals: [InkingStroke, number[]] = []; - // Iterating through all ink strokes in the current freeform collection. - this.childDocs - .filter(doc => doc.type === DocumentType.INK && !doc.dontIntersect) - .forEach(doc => { - // InkingStroke of other ink strokes - const otherInk = DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())?.ComponentView as InkingStroke; - // ink Data of other ink strokes - const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] }; - for (var j = 0; j < otherInkData.length - 3; j += 4) { - const curve: Bezier = InkField.Segment(points, i); // eraser curve - const otherCurve: Bezier = InkField.Segment(otherInkData, j); // other curve - this.bintersects(otherCurve, curve).forEach((val: string | number, k: number) => { - // Converting the Bezier.js Split type to a t-value number. - const t = +val.toString().split('/')[0]; - if (k % 2 === 0) { - // here, add to the map - const inkList = strokeToTVals.get(otherInk); - if (inkList !== undefined) { - const inList = inkList.some(val => Math.abs(val - (t + Math.floor(j / 4))) <= 0.01); - if (!inList) { - inkList.push(t + Math.floor(j/4)); - } - } else { - strokeToTVals.set(otherInk, [t + Math.floor(j/4)]); - } + getOtherInkIntersections = (otherInkDocView: DocumentView, i: number, points: InkData, strokeToTVals: Map<InkingStroke, number[]>): Map<InkingStroke, number[]> => { + // InkingStroke of other ink strokes + const otherInk = otherInkDocView.ComponentView as InkingStroke; + // ink Data of other ink strokes + const { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] }; + for (var j = 0; j < otherInkData.length - 3; j += 4) { + const curve: Bezier = InkField.Segment(points, i); // eraser curve + const otherCurve: Bezier = InkField.Segment(otherInkData, j); // other curve + this.bintersects(otherCurve, curve).forEach((val: string | number, k: number) => { + // Converting the Bezier.js Split type to a t-value number. + const t = +val.toString().split('/')[0]; + if (k % 2 === 0) { + // here, add to the map + const inkList = strokeToTVals.get(otherInk); + if (inkList !== undefined) { + const inList = inkList.some(val => Math.abs(val - (t + Math.floor(j / 4))) <= 0.01); + if (!inList) { + inkList.push(t + Math.floor(j / 4)); } - }); + } else { + strokeToTVals.set(otherInk, [t + Math.floor(j / 4)]); + } } }); + } return strokeToTVals; }; |