aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2021-12-02 00:22:05 -0500
committerbobzel <zzzman@gmail.com>2021-12-02 00:22:05 -0500
commit49e9103721ecfd6d5c900c754f2d88362f64a3ab (patch)
treea1b831f9235767ff8e79285d09c4512074131851
parent1cbbb1b6ebdb2bbf0f05ee1dcbe9922236495f58 (diff)
added shift erase stroke to delete full strokes. added shift drag end of stroke to scale uniformly. added ctrl+p,ctrl+e to switch between eraser/pen. added delete to menu options for tree view items. cleaned up a lot of ink code.
-rw-r--r--src/client/views/GlobalKeyHandler.ts4
-rw-r--r--src/client/views/InkControlPtHandles.tsx18
-rw-r--r--src/client/views/InkStrokeProperties.ts13
-rw-r--r--src/client/views/collections/TreeView.tsx13
-rw-r--r--src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx219
-rw-r--r--src/fields/InkField.ts16
6 files changed, 136 insertions, 147 deletions
diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts
index d5e0ed962..1a4080d81 100644
--- a/src/client/views/GlobalKeyHandler.ts
+++ b/src/client/views/GlobalKeyHandler.ts
@@ -230,6 +230,10 @@ export class KeyManager {
}
}
break;
+ case "e": CurrentUserUtils.SelectedTool = InkTool.Eraser;
+ break;
+ case "p": CurrentUserUtils.SelectedTool = InkTool.Pen;
+ break;
case "o":
const target = SelectionManager.Docs().lastElement();
target && CollectionDockingView.OpenFullScreen(target);
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx
index 76ce73b0d..0996e75d4 100644
--- a/src/client/views/InkControlPtHandles.tsx
+++ b/src/client/views/InkControlPtHandles.tsx
@@ -191,16 +191,16 @@ export class InkEndPtHandles extends React.Component<InkEndProps> {
setupMoveUpEvents(this, e, (e) => {
if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("stretch ink");
// compute stretch factor by finding scaling along axis between start and end points
- const v1 = { x: p1().X - p2().X, y: p1().Y - p2().Y };
- const v2 = { x: e.clientX - p2().X, y: e.clientY - p2().Y };
- const v1len = Math.sqrt(v1.x * v1.x + v1.y * v1.y);
- const v2len = Math.sqrt(v2.x * v2.x + v2.y * v2.y);
+ const v1 = { X: p1().X - p2().X, Y: p1().Y - p2().Y };
+ const v2 = { X: e.clientX - p2().X, Y: e.clientY - p2().Y };
+ const v1len = Math.sqrt(v1.X * v1.X + v1.Y * v1.Y);
+ const v2len = Math.sqrt(v2.X * v2.X + v2.Y * v2.Y);
const scaling = v2len / v1len;
- const v1n = { x: v1.x / v1len, y: v1.y / v1len };
- const v2n = { x: v2.x / v2len, y: v2.y / v2len };
- const angle = Math.acos(v1n.x * v2n.x + v1n.y * v2n.y) * Math.sign(v1.x * v2.y - v2.x * v1.y);
- InkStrokeProperties.Instance.stretchInk([this.props.inkView], scaling, { x: p2().X, y: p2().Y }, v1n);
- InkStrokeProperties.Instance.rotateInk([this.props.inkView], angle, { x: p2().X, y: p2().Y });
+ const v1n = { X: v1.X / v1len, Y: v1.Y / v1len };
+ const v2n = { X: v2.X / v2len, Y: v2.Y / v2len };
+ const angle = Math.acos(v1n.X * v2n.X + v1n.Y * v2n.Y) * Math.sign(v1.X * v2.Y - v2.X * v1.Y);
+ InkStrokeProperties.Instance.stretchInk([this.props.inkView], scaling, p2(), v1n, e.shiftKey);
+ InkStrokeProperties.Instance.rotateInk([this.props.inkView], angle, p2());
return false;
}, action(() => {
this.controlUndo?.end();
diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts
index 9634e6e83..695bdcc5a 100644
--- a/src/client/views/InkStrokeProperties.ts
+++ b/src/client/views/InkStrokeProperties.ts
@@ -205,18 +205,17 @@ export class InkStrokeProperties {
*/
@undoBatch
@action
- stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: { x: number, y: number }, scrVec: { x: number, y: number }) => {
+ stretchInk = (inkStrokes: DocumentView[], scaling: number, scrpt: PointData, scrVec: PointData, scaleUniformly: boolean) => {
this.applyFunction(inkStrokes, (view: DocumentView, ink: InkData, xScale: number, yScale: number, inkStrokeWidth: number) => {
const ptFromScreen = view.ComponentView?.ptFromScreen;
const ptToScreen = view.ComponentView?.ptToScreen;
return !ptToScreen || !ptFromScreen ? ink :
ink.map(ptToScreen).map(i => {
- const pvec = { X: i.X - scrpt.x, Y: i.Y - scrpt.y };
- const svec = pvec.X * scrVec.x * scaling + pvec.Y * scrVec.y * scaling;
- const ovec = -pvec.X * scrVec.y + pvec.Y * (scrVec.x);
- const newscrpt = { X: scrpt.x + svec * scrVec.x - ovec * scrVec.y, Y: scrpt.y + svec * scrVec.y + ovec * scrVec.x };
- const newpt = ptFromScreen(newscrpt);
- return newpt;
+ const pvec = { X: i.X - scrpt.X, Y: i.Y - scrpt.Y };
+ const svec = pvec.X * scrVec.X * scaling + pvec.Y * scrVec.Y * scaling;
+ const ovec = -pvec.X * scrVec.Y * (scaleUniformly ? scaling : 1) + pvec.Y * scrVec.X * (scaleUniformly ? scaling : 1);
+ const newscrpt = { X: scrpt.X + svec * scrVec.X - ovec * scrVec.Y, Y: scrpt.Y + svec * scrVec.Y + ovec * scrVec.X };
+ return ptFromScreen(newscrpt);
});
});
}
diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx
index d8f984601..eedb353e3 100644
--- a/src/client/views/collections/TreeView.tsx
+++ b/src/client/views/collections/TreeView.tsx
@@ -254,9 +254,7 @@ export class TreeView extends React.Component<TreeViewProps> {
TreeView._editTitleOnLoad = { id: folder[Id], parent: this.props.parentTreeView };
return this.props.addDocument(folder);
}
- deleteFolder = () => {
- return this.props.removeDoc?.(this.doc);
- }
+ deleteItem = () => this.props.removeDoc?.(this.doc);
preTreeDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => {
const dragData = de.complete.docDragData;
@@ -531,16 +529,16 @@ export class TreeView extends React.Component<TreeViewProps> {
}
contextMenuItems = () => {
const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "New Folder" };
- const deleteFolder = { script: ScriptField.MakeFunction(`scriptContext.deleteFolder()`, { scriptContext: "any" })!, icon: "folder-plus", label: "Delete Folder" };
- const folderOp = this.childDocs?.length ? makeFolder : deleteFolder;
+ const deleteItem = { script: ScriptField.MakeFunction(`scriptContext.deleteItem()`, { scriptContext: "any" })!, icon: "folder-plus", label: "Delete" };
+ const folderOp = this.childDocs?.length ? [makeFolder] : [];
const openAlias = { script: ScriptField.MakeFunction(`openOnRight(getAlias(self))`)!, icon: "copy", label: "Open Alias" };
const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: "eye", label: "Focus or Open" };
- return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? [folderOp] :
+ return [...this.props.contextMenuItems.filter(mi => !mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result), ... (this.doc.isFolder ? folderOp :
Doc.IsSystem(this.doc) ? [] :
this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) ?
[openAlias, makeFolder] :
this.doc.viewType === CollectionViewType.Docking ? [] :
- [openAlias, focusDoc])];
+ [deleteItem, openAlias, focusDoc])];
}
childContextMenuItems = () => {
const customScripts = Cast(this.doc.childContextMenuScripts, listSpec(ScriptField), []);
@@ -784,6 +782,7 @@ export class TreeView extends React.Component<TreeViewProps> {
const docs = this.props.treeView.onTreeDrop(de, (docs: Doc[]) => this.dropDocuments(docs, before, inside, "copy", undefined, false));
}
+
render() {
TraceMobx();
const hideTitle = this.doc.treeViewHideHeader || this.props.treeView.outlineMode;
diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
index ceee4051b..19c3bf745 100644
--- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
+++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx
@@ -1,11 +1,12 @@
-import { action, computed, IReactionDisposer, observable, reaction, runInAction, ObservableMap } from "mobx";
+import { Bezier } from "bezier-js";
+import { action, computed, IReactionDisposer, observable, reaction, runInAction } from "mobx";
import { observer } from "mobx-react";
import { computedFn } from "mobx-utils";
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, PointData, Intersection, Segment } from "../../../../fields/InkField";
+import { InkData, InkField, InkTool, PointData, Segment } from "../../../../fields/InkField";
import { List } from "../../../../fields/List";
import { ObjectField } from "../../../../fields/ObjectField";
import { RichTextField } from "../../../../fields/RichTextField";
@@ -27,14 +28,15 @@ import { InteractionUtils } from "../../../util/InteractionUtils";
import { LinkManager } from "../../../util/LinkManager";
import { SearchUtil } from "../../../util/SearchUtil";
import { SelectionManager } from "../../../util/SelectionManager";
+import { ColorScheme } from "../../../util/SettingsManager";
import { SnappingManager } from "../../../util/SnappingManager";
import { Transform } from "../../../util/Transform";
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, InkingStroke, SetActiveInkWidth, SetActiveFillColor, SetActiveInkColor } from "../../InkingStroke";
+import { GestureOverlay } from "../../GestureOverlay";
+import { ActiveArrowEnd, ActiveArrowStart, ActiveDash, ActiveFillColor, ActiveInkBezierApprox, ActiveInkColor, ActiveInkWidth, InkingStroke, SetActiveInkColor, SetActiveInkWidth } from "../../InkingStroke";
import { LightboxView } from "../../LightboxView";
import { CollectionFreeFormDocumentView } from "../../nodes/CollectionFreeFormDocumentView";
import { DocFocusOptions, DocumentView, DocumentViewProps, ViewAdjustment, ViewSpecPrefix } from "../../nodes/DocumentView";
@@ -50,10 +52,6 @@ import { CollectionFreeFormRemoteCursors } from "./CollectionFreeFormRemoteCurso
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",
@@ -115,8 +113,6 @@ 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>();
@@ -439,28 +435,29 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
@action
onPointerDown = (e: React.PointerEvent): void => {
- if (e.nativeEvent.cancelBubble || InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) || InteractionUtils.IsType(e, InteractionUtils.PENTYPE) ||
- ([InkTool.Pen, InkTool.Highlighter].includes(CurrentUserUtils.SelectedTool))) {
- return;
- }
- this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
+ this._downX = this._lastX = e.pageX;
+ this._downY = this._lastY = e.pageY;
if (e.button === 0 && !e.altKey && !e.ctrlKey && this.props.isContentActive(true)) {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- document.addEventListener("pointermove", this.onPointerMove);
- document.addEventListener("pointerup", this.onPointerUp);
- // if not using a pen and in no ink mode
- if (CurrentUserUtils.SelectedTool === InkTool.None) {
- this._downX = this._lastX = e.pageX;
- this._downY = this._lastY = e.pageY;
- }
- // eraser plus anything else mode
- else {
- this._batch = UndoManager.StartBatch("collectionErase");
- this._prevPoint = { X: e.clientX, Y: e.clientY };
- e.stopPropagation();
- e.preventDefault();
- }
+ if (!e.nativeEvent.cancelBubble &&
+ !this.props.Document._isGroup && // group freeforms don't pan when dragged -- instead let the event go through to allow the group itself to drag
+ !InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) &&
+ !InteractionUtils.IsType(e, InteractionUtils.PENTYPE))
+ switch (CurrentUserUtils.SelectedTool) {
+ case InkTool.Highlighter:
+ case InkTool.Pen: break; // the GestureOverlay handles ink stroke input -- either as gestures, or drying as ink strokes that are added to document views
+ case InkTool.Eraser:
+ document.addEventListener("pointermove", this.onEraserMove);
+ document.addEventListener("pointerup", this.onEraserUp);
+ this._batch = UndoManager.StartBatch("collectionErase");
+ e.stopPropagation();
+ e.preventDefault();
+ break;
+ case InkTool.None:
+ this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY));
+ document.addEventListener("pointermove", this.onPointerMove);
+ document.addEventListener("pointerup", this.onPointerUp);
+ break;
+ }
}
}
@@ -600,6 +597,16 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
}
}
}
+ @action
+ onEraserUp = (e: PointerEvent): void => {
+ if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
+ document.removeEventListener("pointermove", this.onEraserMove);
+ document.removeEventListener("pointerup", this.onEraserUp);
+ this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc));
+ this._deleteList = [];
+ this._batch?.end();
+ }
+ }
@action
onPointerUp = (e: PointerEvent): void => {
@@ -608,12 +615,6 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
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();
- }
}
}
@@ -638,96 +639,82 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
this._lastY = e.clientY;
}
- @action
- 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) {
- if (CurrentUserUtils.SelectedTool === InkTool.None) {
- if (this.tryDragCluster(e, this._hitCluster)) {
- document.removeEventListener("pointermove", this.onPointerMove);
- document.removeEventListener("pointerup", this.onPointerUp);
- }
- else this.pan(e);
- }
- e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
- e.preventDefault();
- }
- }
-
/**
- * Iterates through all intersected ink strokes, determines their segmentation, draws back the non-intersected segments,
+ * 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.
- * @param eraserIntersections The intersections made by the eraser.
+ * However, if Shift is held, then no segmentation is done -- instead any intersected stroke is deleted in its entirety.
*/
- 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");
+ @action
+ onEraserMove = (e: PointerEvent) => {
+ const currPoint = { X: e.clientX, Y: e.clientY };
+ this.getEraserIntersections({ X: this._lastX, Y: this._lastY }, currPoint).forEach(intersect => {
+ if (!this._deleteList.includes(intersect.inkView)) {
+ this._deleteList.push(intersect.inkView);
+ SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.strokeWidth?.toString()) || "1");
+ SetActiveInkColor(StrCast(intersect.inkView.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 =>
+ !e.shiftKey && this.segmentInkStroke(intersect.inkView, intersect.t).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 })
+ .map(p => intersect.inkView.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;
+ intersect.inkView.layoutDoc.opacity = 0.5;
}
});
+ this._lastX = currPoint.X;
+ this._lastY = currPoint.Y;
+
+ e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
+ e.preventDefault();
+ }
+
+ @action
+ onPointerMove = (e: PointerEvent): void => {
+ if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE)) return;
+ if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) {
+ if (this.props.isContentActive(true)) e.stopPropagation();
+ } else if (!e.cancelBubble) {
+ if (this.tryDragCluster(e, this._hitCluster)) {
+ document.removeEventListener("pointermove", this.onPointerMove);
+ document.removeEventListener("pointerup", this.onPointerUp);
+ }
+ else this.pan(e);
+ e.stopPropagation(); // doesn't actually stop propagation since all our listeners are listening to events on 'document' however it does mark the event as cancelBubble=true which we test for in the move event handlers
+ e.preventDefault();
+ }
}
/**
* 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.
+ * @returns an array of tuples containing the intersected ink DocumentView and the t-value where it was intersected
*/
- 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();
+ getEraserIntersections = (lastPoint: { X: number, Y: number }, currPoint: { X: number, Y: number }) => {
+ const eraserMin = { X: Math.min(lastPoint.X, currPoint.X), Y: Math.min(lastPoint.Y, currPoint.Y) };
+ const eraserMax = { X: Math.max(lastPoint.X, currPoint.X), Y: Math.max(lastPoint.Y, currPoint.Y) };
+ return this.childDocs
+ .map(doc => DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView))
+ .filter(inkView => inkView?.ComponentView instanceof InkingStroke)
+ .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)
+ .reduce((intersections, { inkStroke, inkView }) => {
+ const { inkData } = inkStroke.inkScaledData();
+ // Convert from screen space to ink space for the intersection.
+ const prevPointInkSpace = inkStroke.ptFromScreen(lastPoint);
+ const currPointInkSpace = inkStroke.ptFromScreen(currPoint);
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 });
- }
- }
- }
+ const intersects = Array.from(new Set(InkField.Segment(inkData, i).intersects({ // compute all unique intersections
+ p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y },
+ p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y }
+ }) as (number | string)[])); // convert to more manageable union array type
+ // return tuples of the inkingStroke intersected, and the t value of the intersection
+ intersections.push(...intersects.map(t => ({ inkView, t: (+t) + Math.floor(i / 4) })));// convert string t's to numbers and add start of curve segment to convert from local t value to t value along complete curve
}
- });
- return intersections;
+ return intersections;
+ }, [] as { t: number, inkView: DocumentView }[]);
}
/**
@@ -746,23 +733,23 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
// 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 })));
+ const inkSegment = InkField.Segment(inkData, i);
// Getting all t-value intersections of the current curve with all other curves.
- const tVals = this.getInkIntersections(i, ink, curve).sort();
+ const tVals = this.getInkIntersections(i, ink, inkSegment).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.push(inkSegment.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] : [];
+ segment = tVals.length - 1 === index ? [inkSegment.split(t).right] : [];
startSegmentT = docCurveTVal;
});
} else {
- segment.push(curve);
+ segment.push(inkSegment);
}
}
if (excludeT < startSegmentT || excludeT > (inkData.length / 4)) {
@@ -786,7 +773,7 @@ export class CollectionFreeFormView extends CollectionSubView<PanZoomDocument, P
.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 { inkData: otherInkData } = otherInk?.inkScaledData() ?? { inkData: [] };
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) {
diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts
index f61313674..560cf3d63 100644
--- a/src/fields/InkField.ts
+++ b/src/fields/InkField.ts
@@ -3,7 +3,6 @@ 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.
@@ -22,13 +21,6 @@ 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.
@@ -78,6 +70,14 @@ export class InkField extends ObjectField {
this.inkData = data;
}
+ /**
+ * Extacts a simple segment from a compound Bezier curve
+ * @param segIndex the start index of the simple bezier segment to extact (eg., 0, 4, 8, ...)
+ */
+ public static Segment(inkData: InkData, segIndex: number) {
+ return new Bezier(inkData.slice(segIndex, segIndex + 4).map(pt => ({ x: pt.X, y: pt.Y })));
+ }
+
[Copy]() {
return new InkField(this.inkData);
}