diff options
-rw-r--r-- | src/client/views/InkingStroke.tsx | 49 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 141 | ||||
-rw-r--r-- | src/client/views/global/globalScripts.ts | 3 |
3 files changed, 73 insertions, 120 deletions
diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 03acd5393..9e09c0aa9 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -331,55 +331,6 @@ export class InkingStroke extends ViewBoxAnnotatableComponent<FieldViewProps>() ); }; - splitByEraser = (startInkCoordsIn: { X: number; Y: number }, endInkCoordsIn: { X: number; Y: number }) => { - const radius = ActiveEraserWidth() / 2 + 3; // reduce values to avoid extreme radii - 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); @computed get fillColor() { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 14d20eb4a..e2965c1ba 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -738,12 +738,12 @@ 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, * and deletes the original stroke. */ - @action onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => { const currPoint = { X: e.clientX, Y: e.clientY }; @@ -776,7 +776,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ); }); newStrokes && this.addDocument?.(newStrokes); - setTimeout(() => this._eraserLock--); + // setTimeout(() => this._eraserLock--); } // Lower ink opacity to give the user a visual indicator of deletion. intersect.inkView.layoutDoc.opacity = 0; @@ -797,23 +797,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection */ @action onRadiusEraserMove = (e: PointerEvent, down: number[], delta: number[]) => { - // const currZoom = this.zoomScaling(); - // console.log("curr zoom", currZoom); - // console.log("prev zoom", this.prevZoom); 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)); - const intersections: any[] = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint); - // if (intersections[0].size > 0 && currZoom === this.prevZoom) { - const strokeMap: Map<DocumentView, number[]> = intersections[0]; - const eraserStroke = intersections[1]; + const strokeMap: Map<DocumentView, number[]> = this.getRadiusEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint); strokeMap.forEach((intersects, stroke) => { if (!this._deleteList.includes(stroke)) { this._deleteList.push(stroke); SetActiveInkWidth(StrCast(stroke.Document.stroke_width?.toString()) || '1'); SetActiveInkColor(StrCast(stroke.Document.color?.toString()) || 'black'); - const segments = this.radiusErase(stroke, intersects.sort(), eraserStroke); + const segments = this.radiusErase(stroke, intersects.sort()); segments?.forEach(segment => this.forceStrokeGesture( e, @@ -825,12 +819,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection stroke.layoutDoc.opacity = 0; stroke.layoutDoc.dontIntersect = true; }); - // } else if (intersections[0].length > 0) { - // this.prevZoom = currZoom; - // console.log("skipping occurred"); - // } else if (this.prevZoom = currZoom) { - // this.prevZoom = currZoom; - // } return false; }; @@ -852,7 +840,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; /** - * Creates the eraser outline for a radius eraser. The outline be + * Creates the eraser outline for a radius eraser. The outline is used to intersect with ink strokes and determine + * what falls inside the eraser outline. * @param startInkCoordsIn * @param endInkCoordsIn * @param inkStrokeWidth @@ -860,7 +849,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection */ createEraserOutline = (startInkCoordsIn: { X: number; Y: number }, endInkCoordsIn: { X: number; Y: number }, inkStrokeWidth: number) => { // increase radius slightly based on the erased stroke's width, added to make eraser look more realistic - var radius = ActiveEraserWidth() + inkStrokeWidth * 0.2; + var radius = ActiveEraserWidth() + 5 + inkStrokeWidth * 0.1; // add 5 to prevent eraser from being too small 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); @@ -909,7 +898,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; /** - * Ray-tracing algorithm to determine whether a point is inside the eraser outline + * Ray-tracing algorithm to determine whether a point is inside the eraser outline. * @param eraserOutline * @param point * @returns @@ -917,8 +906,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection insideEraserOutline = (eraserOutline: InkData, point: { X: number; Y: number }) => { var isInside = false; if (!isNaN(eraserOutline[0].X) && !isNaN(eraserOutline[0].Y)) { - let minX = eraserOutline[0].X, maxX = eraserOutline[0].X; // prettier-ignore - let minY = eraserOutline[0].Y, maxY = eraserOutline[0].Y; // prettier-ignore + let minX = eraserOutline[0].X, maxX = eraserOutline[0].X; + let minY = eraserOutline[0].Y, maxY = eraserOutline[0].Y; for (let i = 1; i < eraserOutline.length; i++) { const currPoint: { X: number; Y: number } = eraserOutline[i]; minX = Math.min(currPoint.X, minX); @@ -985,14 +974,18 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; /** - * Same as getEraserIntersections but specific to the radius eraser. Populates a Map of each intersected DocumentView - * to the t-values where the eraser intersected it, then returns this map. - * @returns + * Same as getEraserIntersections but specific to the radius eraser. The key difference is that the radius eraser + * will often intersect multiple strokes, depending on what strokes are inside the eraser. Populates a Map of each + * intersected DocumentView to the t-values where the eraser intersected it, then returns this map. + * @returns */ getRadiusEraserIntersections = (lastPoint: { X: number; Y: number }, currPoint: { X: number; Y: number }) => { - const eraserRadius = ActiveEraserWidth() / this.zoomScaling(); - const eraserMin = { X: Math.min(lastPoint.X, currPoint.X) - eraserRadius, Y: Math.min(lastPoint.Y, currPoint.Y) - eraserRadius }; - const eraserMax = { X: Math.max(lastPoint.X, currPoint.X) + eraserRadius, Y: Math.max(lastPoint.Y, currPoint.Y) + eraserRadius }; + // set distance of the eraser's bounding box based on the zoom + var boundingBoxDist = ActiveEraserWidth() + 5; + this.zoomScaling() < 1 ? boundingBoxDist = boundingBoxDist / (this.zoomScaling() * 1.5) : boundingBoxDist *= this.zoomScaling(); + + const eraserMin = { X: Math.min(lastPoint.X, currPoint.X) - boundingBoxDist, Y: Math.min(lastPoint.Y, currPoint.Y) - boundingBoxDist }; + const eraserMax = { X: Math.max(lastPoint.X, currPoint.X) + boundingBoxDist, Y: Math.max(lastPoint.Y, currPoint.Y) + boundingBoxDist }; const strokeToTVals = new Map<DocumentView, number[]>(); const intersectingStrokes = this.childDocs .map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())) @@ -1006,13 +999,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection eraserMax.X >= inkViewBounds.left && eraserMax.Y >= inkViewBounds.top ); - const erasers: InkData[] = []; + intersectingStrokes.forEach(({ inkStroke, inkView }) => { const { inkData, inkStrokeWidth } = inkStroke.inkScaledData(); const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint); const currPointInkSpace = inkStroke.ptFromScreen(currPoint); const eraserInkData = this.createEraserOutline(prevPointInkSpace, currPointInkSpace, inkStrokeWidth).inkData; - // erasers.push(eraserInkData); // add the ends of the stroke in as "intersections" if (this.insideEraserOutline(eraserInkData, inkData[0])) { @@ -1039,7 +1031,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // here, add to the map const inkList = strokeToTVals.get(inkView); if (inkList !== undefined) { - const tValOffset = ActiveEraserWidth() / 1000; // to prevent tVals from being added when too close, but scaled by eraser width + const tValOffset = ActiveEraserWidth() / 1050; // to prevent tVals from being added when too close, but scaled by eraser width const inList = inkList.some(val => Math.abs(val - (t + Math.floor(i / 4))) <= tValOffset); if (!inList) { inkList.push(t + Math.floor(i / 4)); @@ -1052,42 +1044,34 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } }); - return [strokeToTVals, erasers]; + return strokeToTVals; }; /** - * Splits the passed in ink stroke at the intersection t values, taking out the erased parts. + * Splits the passed in ink stroke at the intersection t values, taking out the erased parts. * Operates in pairs of t values, where the first t value is the start of the erased portion and the following t value is the end. * @param ink the ink stroke DocumentView to split * @param tVals all the t values to split the ink stroke at * @returns a list of the new segments with the erased part removed */ @action - radiusErase = (ink: DocumentView, tVals: number[], erasers: InkData[]): Segment[] => { + radiusErase = (ink: DocumentView, tVals: number[]): Segment[] => { const segments: Segment[] = []; - var eraser: Segment = []; - // for (var i = 0; i < erasers.length; i ++) { - // for (var j = 0; j < erasers[i].length - 3; j +=4) { - // eraser.push(InkField.Segment(erasers[i], j)); - // } - // segments.push(eraser); - // eraser = []; - // } - const inkStroke = ink?.ComponentView as InkingStroke; const { inkData } = inkStroke.inkScaledData(); var currSegment: Segment = []; + + // any radius erase stroke will always result in even tVals, since the ends are included if (tVals.length % 2 !== 0) { - // any radius erase stroke will always result in even tVals, since the ends are included for (var i = 0; i < inkData.length - 3; i += 4) { currSegment.push(InkField.Segment(inkData, i)); } segments.push(currSegment); - return segments; // want to return the full original stroke + return segments; // return the full original stroke } var continueErasing = false; // used to erase segments if they are completely enclosed in the eraser - var firstSegment: Segment = []; // used to keep track of the first segment for closed curves + var firstSegment: Segment = []; // used to keep track of the first segment for closed curves // early return if nothing to split on if (tVals.length === 0 || (tVals.length === 1 && tVals[0] === 0)) { @@ -1108,15 +1092,18 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (segmentTs.length > 0) { for (var j = 0; j < segmentTs.length; j++) { - if (segmentTs[j] === 0) { // if the first end of the segment is within the eraser + if (segmentTs[j] === 0) { + // if the first end of the segment is within the eraser continueErasing = true; - } else if (segmentTs[j] === Math.floor(inkData.length / 4) + 1) { // the last end + } else if (segmentTs[j] === Math.floor(inkData.length / 4) + 1) { + // the last end break; } else { if (!continueErasing) { currSegment.push(inkBezier.split(0, segmentTs[j] - currCurveT)); continueErasing = true; - } else { // we've reached the end of the part to take out... + } else { + // we've reached the end of the part to take out... continueErasing = false; if (currSegment.length > 0) { segments.push(currSegment); // ...so we add it to the list and reset currSegment @@ -1168,7 +1155,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection for (var i = 0; i < inkData.length - 3; i += 4) { const inkSegment: Bezier = InkField.Segment(inkData, i); var currIntersects = this.getInkIntersections(i, ink, inkSegment).sort(); - // get current segments intersections (if any) and add the curve index + // get current segment's intersections (if any) and add the curve index currIntersects = currIntersects.filter(tVal => tVal > 0 && tVal < 1).map(tVal => tVal + Math.floor(i / 4)); if (currIntersects.length) { intersections = [...intersections, ...currIntersects]; @@ -1181,6 +1168,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection var isClosedCurve = false; if (InkingStroke.IsClosed(inkData)) { isClosedCurve = true; + if (intersections.length === 1) { // delete whole stroke if a closed curve has 1 intersection + return segments; + } } if (intersections.length) { @@ -1190,19 +1180,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // find the segments that need to be split var splitSegment1 = -1; // stays -1 if left end is deleted var splitSegment2 = -1; // stays -1 if right end is deleted - if (closestTs[0] !== -1 && closestTs[1] !== -1) { - // if not on the ends + if (closestTs[0] !== -1 && closestTs[1] !== -1) { // if not on the ends splitSegment1 = segmentIndexes[closestTs[0]]; splitSegment2 = segmentIndexes[closestTs[1]]; - } else if (closestTs[0] === -1) { - // for a curve before an intersection + } else if (closestTs[0] === -1) { // for a curve before an intersection splitSegment2 = segmentIndexes[closestTs[1]]; - } else { - // for a curve after an intersection + } else { // for a curve after an intersection splitSegment1 = segmentIndexes[closestTs[0]]; } - - // so here splitSegment1 and splitSegment2 will be the index(es) of the segment(s) to split + // so by this point splitSegment1 and splitSegment2 will be the index(es) of the segment(s) to split var hasSplit = false; var continueErasing = false; @@ -1236,7 +1222,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } else if (splitSegment1 === -1) { // case where first end is erased if (currCurveT === splitSegment2) { - segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, 1)); + if (isClosedCurve && currCurveT === segmentIndexes.lastElement()) { + segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, intersections.lastElement() - currCurveT)); + continueErasing = true; + } else { + segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, 1)); + } hasSplit = true; } else { if (isClosedCurve && currCurveT === segmentIndexes.lastElement()) { @@ -1249,13 +1240,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } else { // case where last end is erased if (currCurveT === segmentIndexes[0] && isClosedCurve) { - segment1.push(inkSegment.split(intersections[0] - currCurveT, 1)); + if (isClosedCurve && currCurveT === segmentIndexes.lastElement()) { + segment1.push(inkSegment.split(intersections[0] - currCurveT, intersections.lastElement() - currCurveT)); + continueErasing = true; + } else { + segment1.push(inkSegment.split(intersections[0] - currCurveT, 1)); + } hasSplit = true; } else if (currCurveT === splitSegment1) { segment1.push(inkSegment.split(0, intersections[closestTs[0]] - currCurveT)); hasSplit = true; + continueErasing = true; } else { - if ((isClosedCurve && hasSplit) || (!isClosedCurve && !hasSplit)) { + if ((isClosedCurve && hasSplit && !continueErasing) || (!isClosedCurve && !hasSplit)) { segment1.push(inkSegment); } } @@ -1311,6 +1308,7 @@ 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) => { @@ -2045,14 +2043,19 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onCursorMove = (e: React.PointerEvent) => { this._eraserX = e.clientX; this._eraserY = e.clientY; - if (this._eraserX < 0 || this._eraserY < 0) { - this._showEraserCircle = false; - } else { - this._showEraserCircle = true; - } // super.setCursorPosition(this.getTransform().transformPoint(e.clientX, e.clientY)); }; + @action + onMouseLeave = () => { + this._showEraserCircle = false; + }; + + @action + onMouseEnter = () => { + this._showEraserCircle = true; + }; + @undoBatch promoteCollection = () => { const childDocs = this.childDocs.slice(); @@ -2321,6 +2324,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._oldWheel = r; // prevent wheel events from passivly propagating up through containers r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); + r?.addEventListener('mouseleave', this.onMouseLeave); + r?.addEventListener('mouseenter', this.onMouseEnter); }} onWheel={this.onPointerWheel} onClick={this.onClick} @@ -2336,15 +2341,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection width: `${100 / (this.nativeDimScaling || 1)}%`, height: this._props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`, }}> - {Doc.ActiveTool === InkTool.RadiusEraser && ( + {(Doc.ActiveTool === InkTool.RadiusEraser && this._showEraserCircle) && ( <div onPointerMove={this.onCursorMove} style={{ position: 'fixed', left: this._eraserX - 60, top: this._eraserY - 100, - width: ActiveEraserWidth() * 2, - height: ActiveEraserWidth() * 2, + width: (ActiveEraserWidth() + 5) * 2, + height: (ActiveEraserWidth() + 5) * 2, borderRadius: '50%', border: '1px solid gray', transform: 'translate(-50%, -50%)', diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 19741e2e0..0a8434148 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -362,9 +362,6 @@ ScriptingGlobals.add(function activeEraserTool() { // toggle: Set overlay status of selected document ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', value: any, checkResult?: boolean) { const selected = SelectionManager.Docs.lastElement() ?? Doc.UserDoc(); - if (option === 'eraserWidth') { - console.log('eraserWidth', value); - } // prettier-ignore const map: Map<'inkMask' | 'labels' | 'fillColor' | 'strokeWidth' | 'strokeColor' | 'eraserWidth', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([ ['inkMask', { |