diff options
-rw-r--r-- | src/client/util/CurrentUserUtils.ts | 10 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 1 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 347 | ||||
-rw-r--r-- | src/client/views/global/globalScripts.ts | 10 |
4 files changed, 214 insertions, 154 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index b1673ff1c..3ee1b42aa 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -752,13 +752,13 @@ pie title Minerals in my tap water return [ { title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen-nib", toolType: "pen", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }}, { title: "Write", toolTip: "Write (Ctrl+Shift+P)", btnType: ButtonType.ToggleButton, icon: "pen", toolType: "write", scripts: {onClick:'{ return setActiveTool(this.toolType, false, _readOnly_);}' }, funcs: {hidden:"IsNoviceMode()" }}, - { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, toolType:"eraser", scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}, + { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.MultiToggleButton, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'}, funcs: {toolType:"activeEraserTool()"}, subMenu: [ { title: "Stroke", toolTip: "Stroke Erase", btnType: ButtonType.ToggleButton, icon: "eraser", toolType:InkTool.StrokeEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} }, - { title: "Segment", toolTip: "Segment Erase", btnType: ButtonType.ToggleButton, icon: "xmarks-lines",toolType:InkTool.SegmentEraser,ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} }, + { title: "Segment", toolTip: "Segment Erase", btnType: ButtonType.ToggleButton, icon: "xmark",toolType:InkTool.SegmentEraser,ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} }, { title: "Radius", toolTip: "Radius Erase", btnType: ButtonType.ToggleButton, icon: "circle-xmark",toolType:InkTool.RadiusEraser, ignoreClick: true, scripts: {onClick: '{ return setActiveTool(this.toolType, false, _readOnly_);}'} }, - ]}, - { title: "Eraser Width", toolTip: "Eraser Width", btnType: ButtonType.NumberSliderButton, toolType: "eraserWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1, funcs: {hidden:"isRadiusEraser()" }}, + ]}, + { title: "Eraser Width", toolTip: "Eraser Width", btnType: ButtonType.NumberSliderButton, toolType: "eraserWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(this.toolType, value, _readOnly_);}'}, numBtnMin: 1, funcs: {hidden:"NotRadiusEraser()"}}, { title: "Circle", toolTip: "Circle (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "circle", toolType:GestureUtils.Gestures.Circle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType:GestureUtils.Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType:GestureUtils.Gestures.Line, scripts: {onClick:`{ return setActiveTool(this.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(this.toolType, true, _readOnly_);}`} }, @@ -1119,7 +1119,7 @@ pie title Minerals in my tap water ScriptingGlobals.add(function MySharedDocs() { return Doc.MySharedDocs; }, "document containing all shared Docs"); ScriptingGlobals.add(function IsExploreMode() { return SnappingManager.ExploreMode; }, "is Dash in exploration mode"); ScriptingGlobals.add(function IsNoviceMode() { return Doc.noviceMode; }, "is Dash in novice mode"); -ScriptingGlobals.add(function isRadiusEraser() { return !(Doc.ActiveTool === InkTool.RadiusEraser); }, "is the eraser selected"); +ScriptingGlobals.add(function NotRadiusEraser() { return Doc.ActiveTool !== InkTool.RadiusEraser; }, "is the active tool anything but the radius eraser"); ScriptingGlobals.add(function toggleComicMode() { Doc.UserDoc().renderStyle = Doc.UserDoc().renderStyle === "comic" ? undefined : "comic"; }, "switches between comic and normal document rendering"); ScriptingGlobals.add(function importDocument() { return CurrentUserUtils.importDocument(); }, "imports files from device directly into the import sidebar"); ScriptingGlobals.add(function setInkToolDefaults() { Doc.ActiveTool = InkTool.None; }); diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 56db0c488..d0b3221b4 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -361,6 +361,7 @@ export class MainView extends ObservableReactComponent<{}> { fa.faDeleteLeft, fa.faXmarksLines, fa.faCircleXmark, + fa.faXmark, fa.faExclamation, fa.faFileAlt, fa.faFileAudio, diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index eedee0b18..2e174be30 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -1,5 +1,7 @@ +import { inside } from '@turf/turf'; import { Bezier } from 'bezier-js'; import { Colors } from 'browndash-components'; +import { validationResult } from 'express-validator'; import { action, computed, IReactionDisposer, makeObservable, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; @@ -35,7 +37,7 @@ import { Timeline } from '../../animationtimeline/Timeline'; import { ContextMenu } from '../../ContextMenu'; import { GestureOverlay } from '../../GestureOverlay'; import { CtrlKey } from '../../GlobalKeyHandler'; -import { ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; +import { ActiveEraserWidth, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from '../../InkingStroke'; import { LightboxView } from '../../LightboxView'; import { CollectionFreeFormDocumentView } from '../../nodes/CollectionFreeFormDocumentView'; import { SchemaCSVPopUp } from '../../nodes/DataVizBox/SchemaCSVPopUp'; @@ -618,11 +620,15 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views case InkTool.StrokeEraser: 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.RadiusEraser: + this._batch = UndoManager.StartBatch('collectionErase'); + this._eraserPts.length = 0; + setupMoveUpEvents(this, e, this.onRadiusEraserMove, this.onEraserUp, emptyFunction); + break; case InkTool.None: if (!(this._props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) { this._hitCluster = this.pickCluster(this.screenToFreeformContentsXf.transformPoint(e.clientX, e.clientY)); @@ -734,7 +740,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection * 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. - * However, if Shift is held, then no segmentation is done -- instead any intersected stroke is deleted in its entirety. */ @action onEraserMove = (e: PointerEvent, down: number[], delta: number[]) => { @@ -750,10 +755,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // create a new curve by appending all curves of the current segment together in order to render a single new stroke. if (Doc.ActiveTool !== InkTool.StrokeEraser) { this._eraserLock++; - 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: this._eraserPts[0][0], Y: this._eraserPts[0][1] }, currPoint); + const segments = 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 segments?.forEach(segment => this.forceStrokeGesture( e, @@ -764,12 +766,44 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection setTimeout(() => this._eraserLock--); } // Lower ink opacity to give the user a visual indicator of deletion. - intersect.inkView.layoutDoc.opacity = 0.2; + intersect.inkView.layoutDoc.opacity = 0; intersect.inkView.layoutDoc.dontIntersect = true; } }); return false; }; + + @action + onRadiusEraserMove = (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. + 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'); + this._eraserLock++; + // create a new curve by appending all curves of the current segment together in order to render a single new stroke. + const segments = this.radiusErase(stroke, intersects.sort()); + segments?.forEach(segment => + this.forceStrokeGesture( + e, + GestureUtils.Gestures.Stroke, + segment.reduce((data, curve) => [...data, ...curve.points.map(p => stroke.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]) + ) + ); + setTimeout(() => this._eraserLock--); + } + // Lower ink opacity to give the user a visual indicator of deletion. + stroke.layoutDoc.opacity = 0; + stroke.layoutDoc.dontIntersect = true; + }); + return false; + }; + forceStrokeGesture = (e: PointerEvent, gesture: GestureUtils.Gestures, points: InkData, text?: any) => { this.onGesture(e, new GestureUtils.GestureEvent(gesture, points, GestureOverlay.getBounds(points), text)); }; @@ -788,7 +822,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; createEraserOutline = (startInkCoordsIn: { X: number; Y: number }, endInkCoordsIn: { X: number; Y: number }) => { - const radius = ActiveEraserWidth() / 2 + 3; // reduce values to avoid extreme radii + const radius = ActiveEraserWidth() + 3; // add 3 to avoid eraser being too thin 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); @@ -837,6 +871,39 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; /** + * Ray-tracing algorithm to determine whether a point is inside the eraser outline + * @param eraserOutline + * @param point + * @returns + */ + insideEraserOutline = (eraserOutline: InkData, point: { X: number; Y: number }) => { + var isInside = false; + if (!isNaN(eraserOutline[0].X) && !isNaN(eraserOutline[0].Y)) { + var minX = eraserOutline[0].X, maxX = eraserOutline[0].X; + var minY = eraserOutline[0].Y, maxY = eraserOutline[0].Y; + for (var i = 1; i < eraserOutline.length; i++) { + const currPoint: {X: number, Y: number} = eraserOutline[i]; + minX = Math.min(currPoint.X, minX); + maxX = Math.max(currPoint.X, maxX); + minY = Math.min(currPoint.Y, minY); + maxY = Math.max(currPoint.Y, maxY); + } + + if (point.X < minX || point.X > maxX || point.Y < minY || point.Y > maxY) { + return false; + } + + for (var i = 0, j = eraserOutline.length - 1; i < eraserOutline.length; j = i, i++) { + if ((eraserOutline[i].Y > point.Y) != (eraserOutline[j].Y > point.Y) && + point.X < (eraserOutline[j].X - eraserOutline[i].X) * (point.Y - eraserOutline[i].Y) / (eraserOutline[j].Y - eraserOutline[i].Y) + eraserOutline[i].X ) { + isInside = !isInside; + } + } + } + return isInside; + }; + + /** * Determines if the Eraser tool has intersected with an ink stroke in the current freeform collection. * @returns an array of tuples containing the intersected ink DocumentView and the t-value where it was intersected */ @@ -880,94 +947,141 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection ); }; + getRadiusEraserIntersections = (lastPoint: { X: number; Y: number }, currPoint: { X: number; Y: number }) => { + const eraserRadius = ActiveEraserWidth() + 3; + 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}; + const strokeToTVals = new Map<DocumentView, number[]>(); + const intersectingStrokes = this.childDocs + .map(doc => DocumentManager.Instance.getDocumentView(doc, this.DocumentView?.())) + .filter(inkView => inkView?.ComponentView instanceof InkingStroke) // filter to all inking strokes + .map(inkView => ({ inkViewBounds: inkView!.getBounds, inkStroke: inkView!.ComponentView as InkingStroke, inkView: inkView! })) + .filter( + ({ inkViewBounds }) => + inkViewBounds && // bounding box of eraser segment and ink stroke overlap + eraserMin.X <= inkViewBounds.right && + eraserMin.Y <= inkViewBounds.bottom && + eraserMax.X >= inkViewBounds.left && + eraserMax.Y >= inkViewBounds.top + ); + console.log("itersectnig strokes", intersectingStrokes); + intersectingStrokes.forEach(({ inkStroke, inkView }) => { + const { inkData } = inkStroke.inkScaledData(); + const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint); + const currPointInkSpace = inkStroke.ptFromScreen(currPoint); + const eraserInkData = this.createEraserOutline(prevPointInkSpace, currPointInkSpace).inkData; + // add the ends of the stroke in as "intersections" + if (this.insideEraserOutline(eraserInkData, inkData[0])) { + strokeToTVals.set(inkView, [0]); + } + if (this.insideEraserOutline(eraserInkData, inkData[inkData.length - 1])) { + const inkList = strokeToTVals.get(inkView); + if (inkList !== undefined) { + inkList.push(Math.floor(inkData.length / 4) + 1); + } else { + strokeToTVals.set(inkView, [Math.floor(inkData.length / 4) + 1]); + } + } + for (var i = 0; i < inkData.length - 3; i += 4) { + // iterate over each segment of bezier curve + for (var j = 0; j < eraserInkData.length - 3; j += 4) { + const intersectCurve: Bezier = InkField.Segment(inkData, i); // other curve + const eraserCurve: Bezier = InkField.Segment(eraserInkData, j); // eraser curve + this.bintersects(intersectCurve, eraserCurve).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(inkView); + if (inkList !== undefined) { + const tValOffset = ActiveEraserWidth() / 1030; // 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)); + } + } else { + strokeToTVals.set(inkView, [t + Math.floor(i / 4)]); + } + } + }); + } + } + }); + console.log("strokeToTVals", strokeToTVals); + return strokeToTVals; + }; + + /** + * Splits the passed in ink stroke at the intersection t values. Generally 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, startPt: { X: number; Y: number }, screenEraserPt: { X: number; Y: number }): Segment[] => { + radiusErase = (ink: DocumentView, tVals: number[]): Segment[] => { const segments: Segment[] = []; - const startInkCoords = ink.ComponentView?.ptFromScreen?.(startPt); - const inkCoords = ink.ComponentView?.ptFromScreen?.(screenEraserPt); // coordinates in ink space - 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) { - eraseSegment.push(InkField.Segment(eraserStroke, i)); // for eraser visualization - this.getOtherInkIntersections(ink, i, eraserStroke, strokeToTVals); + const { inkData } = inkStroke.inkScaledData(); + var currSegment: Segment = []; + if (tVals.length % 2 !== 0) { // should always have even tVals + for (var i = 0; i < inkData.length - 3; i +=4) { + currSegment.push(InkField.Segment(inkData, i)); + } + if (currSegment.length > 0) { + segments.push(currSegment); + return segments; + } } - strokeToTVals.forEach((tVals, inkStroke) => { - var segment1: Segment = []; - var segment2: Segment = []; - const { inkData } = inkStroke.inkScaledData(); - tVals.sort(); - console.log('TVALS', inkStroke, tVals); - var hasSplit = false; - var continueErasing = false; - - // below is curve splitting logic - if (tVals.length) { - for (var i = 0; i < inkData.length - 3; i += 4) { - const inkSegment: Bezier = InkField.Segment(inkData, i); - const currCurveT = Math.floor(i / 4); + var continueErasing = false; + var firstSegment: Segment = []; + // early return if nothing to split on + if (tVals.length === 0 || (tVals.length === 1 && tVals[0] === 0)) { + return segments; + } - if (tVals.length === 2) { - if (tVals[0] > currCurveT && tVals[0] < currCurveT + 1) { - segment1.push(inkSegment.split(0, tVals[0] - currCurveT)); + for (var i = 0; i < inkData.length - 3; i += 4) { + const currCurveT = Math.floor(i/4); + const inkBezier: Bezier = InkField.Segment(inkData, i); + const segmentTs = tVals.filter(t => t >= currCurveT && t < currCurveT + 1); + + if (segmentTs.length > 0) { + for (var j = 0; j < segmentTs.length; j++) { + // if the first end of the segment is within the eraser + if (segmentTs[j] === 0 ) { + continueErasing = true; + } else if (segmentTs[j] === Math.floor(inkData.length / 4) + 1) { + break; + }else { + if (!continueErasing) { + currSegment.push(inkBezier.split(0, segmentTs[j] - currCurveT)); continueErasing = true; - if (tVals[1] > currCurveT && tVals[1] < currCurveT + 1) { - segment2.push(inkSegment.split(tVals[1] - currCurveT, 1)); - continueErasing = false; - hasSplit = true; - } - } else if (tVals[1] > currCurveT && tVals[1] < currCurveT + 1) { - segment2.push(inkSegment.split(tVals[1] - currCurveT, 1)); - continueErasing = false; - hasSplit = true; - } else if (!continueErasing) { - if (hasSplit) { - segment2.push(inkSegment); - } else { - segment1.push(inkSegment); - } - } - } else if (tVals.length === 1) { - if (tVals[0] > currCurveT && tVals[0] < currCurveT + 1) { - // 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(splits.left); - hasSplit = true; - } else { - segment1.push(splits.right); - hasSplit = true; - } } else { - if (tVals[0] < Math.floor(inkData.length / 4) - tVals[0] && hasSplit) { - segment1.push(inkSegment); - } else if (tVals[0] >= Math.floor(inkData.length / 4) - tVals[0] && !hasSplit) { - segment1.push(inkSegment); + continueErasing = false; + if (currSegment.length > 0) { + segments.push(currSegment); + if (firstSegment.length === 0) { + firstSegment = currSegment; + } + currSegment = []; } + currSegment.push(inkBezier.split(segmentTs[j] - currCurveT, 1)); } } } + } else { + if (!continueErasing) { // push the bezier piece if not in the eraser circle + currSegment.push(inkBezier); + } } - if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) { - segments.push(segment1); - } - if (segment2.length && (Math.abs(segment2[0].points[0].x - segment2[0].points.lastElement().x) > 0.5 || Math.abs(segment2[0].points[0].y - segment2[0].points.lastElement().y) > 0.5)) { - segments.push(segment2); + } + if (currSegment.length > 0) { + if (InkingStroke.IsClosed(inkData)) { + currSegment = currSegment.concat(firstSegment); } - }); - - segments.push(eraseSegment); // for eraser visualization + segments.push(currSegment); + } return segments; }; @@ -993,7 +1107,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection 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 - currIntersects = currIntersects.map(tVal => tVal + Math.floor(i / 4)); + currIntersects = currIntersects.filter(tVal => tVal > 0 && tVal < 1).map(tVal => tVal + Math.floor(i / 4)); if (currIntersects.length) { intersections = [...intersections, ...currIntersects]; for (var j = 0; j < currIntersects.length; j++) { @@ -1002,19 +1116,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } - // for closed curve bookkeeping var isClosedCurve = false; - if (intersections[0] === 0) { + if (InkingStroke.IsClosed(inkData)) { isClosedCurve = true; - - // now reduce intersections list to the actual intersections because closed curves have additional ones with itself - const indices = intersections.map((value, index) => (value > 0.0001 && value < Math.floor(inkData.length / 4) ? index : -1)).filter(index => index !== -1); - - // Filter intersections and segmentIndexes based on validIndices - intersections = indices.map(index => intersections[index]); - segmentIndexes = indices.map(index => segmentIndexes[index]); - // intersections = intersections.slice(1, intersections.length ); // take the 0 intersection out - // segmentIndexes = segmentIndexes.slice(1, segmentIndexes.length); // same for indexes } if (intersections.length) { @@ -1068,7 +1172,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } } else if (splitSegment1 === -1) { - // case where left end is erased + // case where first end is erased if (currCurveT === splitSegment2) { segment2.push(inkSegment.split(intersections[closestTs[1]] - currCurveT, 1)); hasSplit = true; @@ -1081,15 +1185,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } } else { - // case where right end is erased + // case where last end is erased if (currCurveT === segmentIndexes[0] && isClosedCurve) { segment1.push(inkSegment.split(intersections[0] - currCurveT, 1)); hasSplit = true; } else if (currCurveT === splitSegment1) { segment1.push(inkSegment.split(0, intersections[closestTs[0]] - currCurveT)); - hasSplit = false; // although this is not technically true, we set to false so the segments after don't get added - // } else if (isClosedCurve && currCurveT === segmentIndexes[1]) { - // segment1.push(inkSegment.split(intersections[0] - currCurveT, 1)); + hasSplit = true; } else { if ((isClosedCurve && hasSplit) || (!isClosedCurve && !hasSplit)) { segment1.push(inkSegment); @@ -1099,6 +1201,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } + if (isClosedCurve && segment1.length > 0 && segment2.length > 0) { + segment2 = segment2.concat(segment1); + segment1 = []; + } + // push 1 or both segments if they are not empty if (segment1.length && (Math.abs(segment1[0].points[0].x - segment1[0].points.lastElement().x) > 0.5 || Math.abs(segment1[0].points[0].y - segment1[0].points.lastElement().y) > 0.5)) { segments.push(segment1); @@ -1203,46 +1310,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return tVals; }; - /** - * 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 - */ - getOtherInkIntersections = (otherInkDocView: DocumentView, i: number, points: InkData, strokeToTVals: Map<InkingStroke, number[]>): Map<InkingStroke, number[]> => { - this.childDocs - .filter(doc => doc.type === DocumentType.INK && !doc.dontIntersect) - .forEach(doc => { - // 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; - }; - @action zoom = (pointX: number, pointY: number, deltaY: number): void => { if (this.Document.isGroup || this.Document[(this._props.viewField ?? '_') + 'freeform_noZoom']) return; diff --git a/src/client/views/global/globalScripts.ts b/src/client/views/global/globalScripts.ts index 5c5f8de03..19741e2e0 100644 --- a/src/client/views/global/globalScripts.ts +++ b/src/client/views/global/globalScripts.ts @@ -341,10 +341,7 @@ function setActiveTool(tool: InkTool | GestureUtils.Gestures, keepPrim: boolean, Doc.UserDoc().activeEraserTool = tool; } // pen or eraser - if (Doc.ActiveTool === tool && tool === InkTool.Eraser) { - Doc.ActiveTool = InkTool.SegmentEraser; - console.log("erase click twice") - } else if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) { + if (Doc.ActiveTool === tool && !GestureOverlay.Instance.InkShape && !keepPrim) { Doc.ActiveTool = InkTool.None; } else { Doc.ActiveTool = tool as any; @@ -362,11 +359,6 @@ ScriptingGlobals.add(function activeEraserTool() { return StrCast(Doc.UserDoc().activeEraserTool, InkTool.StrokeEraser); }, 'returns the current eraser tool'); -// ScriptingGlobals.add(function setEraserProperty(option: 'eraseWidth', value: any, checkResult?: boolean) { -// setInk: (doc: Doc) => (doc[DocData].eraserWidth = NumCast(value)) -// InkingStroke.setEraserRadius(NumCast(value)); -// }) - // 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(); |