diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 152 |
1 files changed, 102 insertions, 50 deletions
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 622189b80..14d20eb4a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -95,8 +95,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection private _panZoomTransitionTimer: any; private _lastX: number = 0; private _lastY: number = 0; - @observable _eraserX: number = 0; - @observable _eraserY: number = 0; private _downX: number = 0; private _downY: number = 0; private _downTime = 0; @@ -107,6 +105,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection private _batch: UndoManager.Batch | undefined = undefined; private _brushtimer: any; private _brushtimer1: any; + private prevZoom: number = 0; public get isAnnotationOverlay() { return this._props.isAnnotationOverlay; @@ -134,6 +133,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @observable _marqueeViewRef = React.createRef<MarqueeView>(); @observable _brushedView: { width: number; height: number; panX: number; panY: number } | undefined = undefined; // highlighted region of freeform canvas used by presentations to indicate a region @observable GroupChildDrag: boolean = false; // child document view being dragged. needed to update drop areas of groups when a group item is dragged. + @observable _eraserX: number = 0; + @observable _eraserY: number = 0; + @observable _showEraserCircle: boolean = false; // to determine whether the radius eraser should show @computed get contentViews() { const viewsMask = this._layoutElements.filter(ele => ele.bounds && !ele.bounds.z && ele.inkMask !== -1 && ele.inkMask !== undefined).map(ele => ele.ele); @@ -747,7 +749,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection 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. + // if (this._eraserLock) return false; // leaving this commented out in case the idea is revisited in the future this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => { if (!this._deleteList.includes(intersect.inkView)) { this._deleteList.push(intersect.inkView); @@ -755,7 +757,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection SetActiveInkColor(StrCast(intersect.inkView.Document.color?.toString()) || 'black'); // 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++; + // this._eraserLock++; 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 const newStrokes = segments?.map(segment => { const points = segment.reduce((data, curve) => [...data, ...curve.points.map(p => intersect.inkView.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })], [] as PointData[]); @@ -784,23 +786,34 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return false; }; + /** + * Erases strokes by intersecting them with a circle of variable radius. Essentially creates an InkField for the + * eraser circle, then determines its intersections with other ink strokes. Each stroke's DocumentView and its + * intersection t-values are put into a map, which gets looped through to take out the erased parts. + * @param e + * @param down + * @param delta + * @returns + */ @action onRadiusEraserMove = (e: PointerEvent, down: number[], delta: number[]) => { - // this._eraserX = e.clientX; - // this._eraserY = e.clientY; + // 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)); - 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); + 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]; + 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()); + const segments = this.radiusErase(stroke, intersects.sort(), eraserStroke); segments?.forEach(segment => this.forceStrokeGesture( e, @@ -808,12 +821,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection 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; }); + // } else if (intersections[0].length > 0) { + // this.prevZoom = currZoom; + // console.log("skipping occurred"); + // } else if (this.prevZoom = currZoom) { + // this.prevZoom = currZoom; + // } return false; }; @@ -834,8 +851,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return false; }; - createEraserOutline = (startInkCoordsIn: { X: number; Y: number }, endInkCoordsIn: { X: number; Y: number }) => { - const radius = ActiveEraserWidth() + 3; // add 3 to avoid eraser being too thin + /** + * Creates the eraser outline for a radius eraser. The outline be + * @param startInkCoordsIn + * @param endInkCoordsIn + * @param inkStrokeWidth + * @returns + */ + 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; 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); @@ -959,8 +984,13 @@ 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 + */ getRadiusEraserIntersections = (lastPoint: { X: number; Y: number }, currPoint: { X: number; Y: number }) => { - const eraserRadius = ActiveEraserWidth() + 3; + 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 }; const strokeToTVals = new Map<DocumentView, number[]>(); @@ -976,12 +1006,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection eraserMax.X >= inkViewBounds.left && eraserMax.Y >= inkViewBounds.top ); - + const erasers: InkData[] = []; intersectingStrokes.forEach(({ inkStroke, inkView }) => { - const { inkData } = inkStroke.inkScaledData(); + const { inkData, inkStrokeWidth } = inkStroke.inkScaledData(); const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint); const currPointInkSpace = inkStroke.ptFromScreen(currPoint); - const eraserInkData = this.createEraserOutline(prevPointInkSpace, currPointInkSpace).inkData; + 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])) { strokeToTVals.set(inkView, [0]); @@ -994,6 +1026,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection 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) { @@ -1006,7 +1039,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection // 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 tValOffset = ActiveEraserWidth() / 1000; // 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)); @@ -1019,33 +1052,43 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } }); - return strokeToTVals; + return [strokeToTVals, erasers]; }; /** - * 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. + * 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[]): Segment[] => { + radiusErase = (ink: DocumentView, tVals: number[], erasers: InkData[]): 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 = []; if (tVals.length % 2 !== 0) { - // should always have even tVals + // 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; + return segments; // want to return the full original stroke } - var continueErasing = false; - var firstSegment: Segment = []; + 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 + // early return if nothing to split on if (tVals.length === 0 || (tVals.length === 1 && tVals[0] === 0)) { for (var i = 0; i < inkData.length - 3; i += 4) { @@ -1055,26 +1098,28 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return segments; } + // loop through all segments of an ink stroke, string together the pieces, excluding the erased parts, + // and push each piece we want to keep to the return list for (var i = 0; i < inkData.length - 3; i += 4) { const currCurveT = Math.floor(i / 4); const inkBezier: Bezier = InkField.Segment(inkData, i); + // filter to this segment's t-values 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) { + 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) { + } 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 { + } else { // we've reached the end of the part to take out... continueErasing = false; if (currSegment.length > 0) { - segments.push(currSegment); + segments.push(currSegment); // ...so we add it to the list and reset currSegment if (firstSegment.length === 0) { firstSegment = currSegment; } @@ -1091,15 +1136,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } } + if (currSegment.length > 0) { + // add the first segment onto the last to avoid fragmentation for closed curves if (InkingStroke.IsClosed(inkData)) { currSegment = currSegment.concat(firstSegment); } segments.push(currSegment); } - if (segments.length === 0) { - console.log("segments is 0"); - } return segments; }; @@ -1219,6 +1263,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection } } + // add the first segment onto the second one for closed curves, so they don't get fragmented into two pieces if (isClosedCurve && segment1.length > 0 && segment2.length > 0) { segment2 = segment2.concat(segment1); segment1 = []; @@ -1439,7 +1484,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const maxPanY = minPanY + // minPanY + scrolling introduced by view scaling + scrolling introduced by layout_fitWidth scale * NumCast(this.dataDoc._panY_max, nativeHeight) + - (!this._props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning + (!this._props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrollaing is handled via a scrollbar, not panning let newPanY = Math.max(minPanY, Math.min(maxPanY, panY)); if (false && NumCast(this.layoutDoc.layout_scrollTop) && NumCast(this.layoutDoc._freeform_scale, minScale) !== minScale) { const relTop = NumCast(this.layoutDoc.layout_scrollTop) / maxScrollTop; @@ -2000,6 +2045,11 @@ 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)); }; @@ -2286,19 +2336,21 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection width: `${100 / (this.nativeDimScaling || 1)}%`, height: this._props.getScrollHeight?.() ?? `${100 / (this.nativeDimScaling || 1)}%`, }}> - {/* <div - onPointerMove={this.onCursorMove} - style={{ - position: 'fixed', - left: this._eraserX, - top: this._eraserY, - width: '30px', - height: '30px', - borderRadius: '50%', - // backgroundColor: 'transparent', - border: '1px solid black', - }} - /> */} + {Doc.ActiveTool === InkTool.RadiusEraser && ( + <div + onPointerMove={this.onCursorMove} + style={{ + position: 'fixed', + left: this._eraserX - 60, + top: this._eraserY - 100, + width: ActiveEraserWidth() * 2, + height: ActiveEraserWidth() * 2, + borderRadius: '50%', + border: '1px solid gray', + transform: 'translate(-50%, -50%)', + }} + /> + )} {this.paintFunc ? ( <FormattedTextBox {...this.props} /> // need this so that any live dashfieldviews will update the underlying text that the code eval reads ) : this._lightboxDoc ? ( |