diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/client/views/GlobalKeyHandler.ts | 4 | ||||
-rw-r--r-- | src/client/views/InkControlPtHandles.tsx | 18 | ||||
-rw-r--r-- | src/client/views/InkStrokeProperties.ts | 13 | ||||
-rw-r--r-- | src/client/views/collections/TreeView.tsx | 13 | ||||
-rw-r--r-- | src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 219 | ||||
-rw-r--r-- | src/fields/InkField.ts | 16 |
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); } |