aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2021-11-29 09:22:51 -0500
committerbobzel <zzzman@gmail.com>2021-11-29 09:22:51 -0500
commit7e6baacfe6c1b55a3fe1807903455f9ff3844d74 (patch)
treeb9fdcf540237be724b540fa8f0b4757efd1ad05c
parent5acef82a26bbbc237d7dac00061d2ca84e731c68 (diff)
parent8405ed2e3ebaf7ba7842a5619e9b252bf5eb9c4c (diff)
Merge branch 'ink_v1'
-rw-r--r--src/client/util/CurrentUserUtils.ts1
-rw-r--r--src/client/views/GestureOverlay.tsx2
-rw-r--r--src/client/views/InkStrokeProperties.ts2
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx170
-rw-r--r--src/client/views/nodes/button/FontIconBox.tsx2
-rw-r--r--src/fields/InkField.ts11
6 files changed, 182 insertions, 6 deletions
diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts
index c9bef5924..b9e62b303 100644
--- a/src/client/util/CurrentUserUtils.ts
+++ b/src/client/util/CurrentUserUtils.ts
@@ -1005,6 +1005,7 @@ export class CurrentUserUtils {
static inkTools(doc: Doc) {
const tools: Button[] = [
{ title: "Pen", toolTip: "Pen (Ctrl+P)", btnType: ButtonType.ToggleButton, icon: "pen", click: 'setActiveInkTool("pen")', checkResult: 'setActiveInkTool("pen" , true)' },
+ { title: "Eraser", toolTip: "Eraser (Ctrl+E)", btnType: ButtonType.ToggleButton, icon: "eraser", click: 'setActiveInkTool("eraser")', checkResult: 'setActiveInkTool("eraser" , true)' },
// { title: "Highlighter", toolTip: "Highlighter (Ctrl+H)", btnType: ButtonType.ToggleButton, icon: "highlighter", click: 'setActiveInkTool("highlighter")', checkResult: 'setActiveInkTool("highlighter", true)' },
{ title: "Circle", toolTip: "Circle (Ctrl+Shift+C)", btnType: ButtonType.ToggleButton, icon: "circle", click: 'setActiveInkTool("circle")', checkResult: 'setActiveInkTool("circle" , true)' },
// { title: "Square", toolTip: "Square (Ctrl+Shift+S)", btnType: ButtonType.ToggleButton, icon: "square", click: 'setActiveInkTool("square")', checkResult: 'setActiveInkTool("square" , true)' },
diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx
index f28485e43..e2193c9ac 100644
--- a/src/client/views/GestureOverlay.tsx
+++ b/src/client/views/GestureOverlay.tsx
@@ -3,7 +3,7 @@ import * as fitCurve from 'fit-curve';
import { action, computed, observable, runInAction } from "mobx";
import { observer } from "mobx-react";
import { Doc } from "../../fields/Doc";
-import { InkData, InkTool } from "../../fields/InkField";
+import { InkData, InkTool, PointData } from "../../fields/InkField";
import { Cast, FieldValue, NumCast } from "../../fields/Types";
import MobileInkOverlay from "../../mobile/MobileInkOverlay";
import { GestureUtils } from "../../pen-gestures/GestureUtils";
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 6687b2bc7..02288bbb5 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -244,7 +244,7 @@ export class InkStrokeProperties {
const deltaX = (snapData.nearestPt.X - ink[controlIndex].X);
const deltaY = (snapData.nearestPt.Y - ink[controlIndex].Y);
const res = this.moveControlPtHandle(inkView, deltaX, deltaY, controlIndex);
- console.log("X= " + snapData.nearestPt.X + " " + snapData.nearestPt.Y);
+ console.log("X = " + snapData.nearestPt.X + " " + snapData.nearestPt.Y);
return res;
}
}
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index 190d0e76a..ceee4051b 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 } 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";
@@ -29,12 +29,12 @@ import { SearchUtil } from "../../../util/SearchUtil";
import { SelectionManager } from "../../../util/SelectionManager";
import { SnappingManager } from "../../../util/SnappingManager";
import { Transform } from "../../../util/Transform";
-import { undoBatch } from "../../../util/UndoManager";
+import { undoBatch, UndoManager } from "../../../util/UndoManager";
import { COLLECTION_BORDER_WIDTH } from "../../../views/global/globalCssVariables.scss";
import { Timeline } from "../../animationtimeline/Timeline";
import { ContextMenu } from "../../ContextMenu";
import { DocumentDecorations } from "../../DocumentDecorations";
-import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth } from "../../InkingStroke";
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkWidth, SetActiveFillColor, SetActiveInkColor } from "../../InkingStroke";
import { LightboxView } from "../../LightboxView";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView";
@@ -51,6 +51,9 @@ import "./CollectionFreeFormView.scss";
import { MarqueeView } from "./MarqueeView";
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",
@@ -98,6 +101,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
private _layoutSizeData = observable.map<string, { width?: number, height?: number }>();
private _cachedPool: Map<string, PoolData> = new Map();
private _lastTap = 0;
+ private _batch: UndoManager.Batch | undefined = undefined;
private get isAnnotationOverlay() { return this.props.isAnnotationOverlay; }
private get scaleFieldKey() { return this.props.scaleField || "_viewScale"; }
@@ -111,6 +115,9 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
@observable _pullDirection: string = "";
@observable _showAnimTimeline = false;
@observable _clusterSets: (Doc[])[] = [];
+ @observable _prevPoint: PointData = { X: -1, Y: -1 };
+ @observable _currPoint: PointData = { X: -1, Y: -1 };
+ @observable _deleteList: DocumentView[] = [];
@observable _timelineRef = React.createRef<Timeline>();
@observable _marqueeRef = React.createRef<HTMLDivElement>();
@observable _keyframeEditing = false;
@@ -449,6 +456,8 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
// eraser plus anything else mode
else {
+ this._batch = UndoManager.StartBatch("collectionErase");
+ this._prevPoint = { X: e.clientX, Y: e.clientY };
e.stopPropagation();
e.preventDefault();
}
@@ -592,12 +601,19 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
}
+ @action
onPointerUp = (e: PointerEvent): void => {
if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
document.removeEventListener("pointermove", this.onPointerMove);
document.removeEventListener("pointerup", this.onPointerUp);
this.removeMoveListeners();
this.removeEndListeners();
+ if (CurrentUserUtils.SelectedTool !== InkTool.None) {
+ this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc));
+ this._prevPoint = this._currPoint = { X: -1, Y: -1 };
+ this._deleteList = [];
+ this._batch?.end();
+ }
}
}
@@ -626,6 +642,11 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
onPointerMove = (e: PointerEvent): void => {
if (this.props.Document._isGroup) return; // groups don't pan when dragged -- instead let the event go through to allow the group itself to drag
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.
+ this.eraseInkStrokes(this.getEraserIntersections());
+ }
if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
if (this.props.isContentActive(true)) e.stopPropagation();
} else if (!e.cancelBubble) {
@@ -641,6 +662,149 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
}
+ /**
+ * 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 => {
+ const ink = intersect.ink;
+ if (ink && !this._deleteList.includes(ink)) {
+ this._deleteList.push(ink);
+ SetActiveInkWidth(StrCast(ink.rootDoc.strokeWidth?.toString()) || "1");
+ SetActiveInkColor(StrCast(ink.rootDoc.color?.toString()) || "black");
+ // create a new curve by appending all curves of the current segment together in order to render a single new stroke.
+ this.segmentInkStroke(ink, intersect.t ?? 0).forEach(segment =>
+ GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke,
+ segment.reduce((data, curve) => [...data, ...curve.points
+ .map(p => ink.ComponentView?.ptToScreen?.({ X: p.x, Y: p.y }) ?? { X: 0, Y: 0 })
+ ], [] as PointData[])));
+ // Lower ink opacity to give the user a visual indicator of deletion.
+ ink.layoutDoc.opacity = 0.5;
+ }
+ });
+ }
+
+ /**
+ * Determines if the Eraser tool has intersected with an ink stroke in the current freeform collection.
+ * @returns A dictionary mapping the t-value intersection of the eraser with the corresponding ink DocumentView.
+ */
+ getEraserIntersections = (): Intersection[] => {
+ const intersections: Intersection[] = [];
+ this.childDocs
+ .filter(doc => doc.type === DocumentType.INK)
+ .forEach(doc => {
+ const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView);
+ const inkStroke = inkView?.ComponentView as InkingStroke;
+ const { inkData } = inkStroke?.inkScaledData();
+ for (var i = 0; i < inkData.length - 3; i += 4) {
+ const array = inkData.slice(i, i + 4);
+ // Converting from screen space to ink space for the intersection.
+ const prevPointInkSpace = inkStroke?.ptFromScreen?.(this._prevPoint);
+ const currPointInkSpace = inkStroke?.ptFromScreen?.(this._currPoint);
+ if (prevPointInkSpace && currPointInkSpace) {
+ const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y })));
+ const intersects = curve.intersects({
+ p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
+ p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }
+ });
+ if (inkView && intersects) {
+ for (const val of intersects) {
+ // Casting t-value from type: (string | number) to number for comparisons.
+ const t = +(Number(val) + Math.floor(i / 4)).toString(); // add start of curve segment to convert from local t value to t value along complete curve
+ 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 });
+ }
+ }
+ }
+ }
+ });
+ return intersections;
+ }
+
+ /**
+ * 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 excludeT 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, excludeT: number): Segment[] => {
+ const segments: Segment[] = [];
+ var segment: Segment = [];
+ var startSegmentT = 0;
+ const { inkData } = (ink?.ComponentView as InkingStroke).inkScaledData();
+ // This iterates through all segments of the curve and splits them where they intersect another curve.
+ // if 'excludeT' is specified, then any segment containing excludeT will be skipped (ie, deleted)
+ for (var i = 0; i < inkData.length - 3; i += 4) {
+ const curve = new Bezier(inkData.slice(i, i + 4).map(p => ({ x: p.X, y: p.Y })));
+ // Getting all t-value intersections of the current curve with all other curves.
+ const tVals = this.getInkIntersections(i, ink, curve).sort();
+ if (tVals.length) {
+ tVals.forEach((t, index) => {
+ const docCurveTVal = t + Math.floor(i / 4);
+ if (excludeT < startSegmentT || excludeT > docCurveTVal) {
+ const localStartTVal = startSegmentT - Math.floor(i / 4);
+ segment.push(curve.split(localStartTVal < 0 ? 0 : localStartTVal, t));
+ segment.length && segments.push(segment);
+ }
+ // start a new segment from the intersection t value
+ segment = tVals.length - 1 === index ? [curve.split(t).right] : [];
+ startSegmentT = docCurveTVal;
+ });
+ } else {
+ segment.push(curve);
+ }
+ }
+ if (excludeT < startSegmentT || excludeT > (inkData.length / 4)) {
+ segment.length && 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 otherInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView)?.ComponentView as InkingStroke;
+ const { inkData: otherInkData } = otherInk.inkScaledData();
+ const otherScreenPts = otherInkData.map(point => otherInk.ptToScreen(point));
+ const otherCtrlPts = otherScreenPts.map(spt => (ink.ComponentView as InkingStroke).ptFromScreen(spt));
+ for (var j = 0; j < otherCtrlPts.length - 3; j += 4) {
+ const neighboringSegment = i === j || i === j - 4 || i === j + 4;
+ // Ensuring that the curve intersected by the eraser is not checked for further ink intersections.
+ if (ink?.Document === otherInk.props.Document && neighboringSegment) continue;
+
+ const otherCurve = new Bezier(otherCtrlPts.slice(j, j + 4).map(p => ({ x: p.X, y: p.Y })));
+ curve.intersects(otherCurve).forEach((val: string | number, i: number) => {
+ // Converting the Bezier.js Split type to a t-value number.
+ const t = +val.toString().split("/")[0];
+ if (i % 2 === 0 && !tVals.includes(t)) tVals.push(t); // bcz: Hack! don't know why but intersection points are doubled from bezier.js (but not identical).
+ });
+ }
+ });
+ return tVals;
+ }
+
handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent<TouchEvent>) => {
if (!e.cancelBubble) {
const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true);
diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx
index 33fa23805..14b1cbb5d 100644
--- a/src/client/views/nodes/button/FontIconBox.tsx
+++ b/src/client/views/nodes/button/FontIconBox.tsx
@@ -766,7 +766,7 @@ Scripting.addGlobal(function setActiveInkTool(tool: string, checkResult?: boolea
Doc.UserDoc().activeInkTool = InkTool.Pen;
GestureOverlay.Instance.InkShape = tool;
}
- } else if (tool) { // pen
+ } else if (tool) { // pen or eraser
if (Doc.UserDoc().activeInkTool === tool && !GestureOverlay.Instance.InkShape) {
Doc.UserDoc().activeInkTool = InkTool.None;
} else {
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index f16e143d8..f61313674 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -3,6 +3,8 @@ import { Scripting } from "../client/util/Scripting";
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 {
@@ -20,6 +22,15 @@ export interface PointData {
Y: number;
}
+export interface Intersection {
+ 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>;