From 4bbfe0fad535033309e352311fae64520b9068a6 Mon Sep 17 00:00:00 2001 From: Lauren Date: Thu, 28 Oct 2021 16:07:11 -0400 Subject: adding eraser tool to menu --- src/client/util/CurrentUserUtils.ts | 1 + src/client/views/GestureOverlay.tsx | 9 ++++++++- src/client/views/InkStrokeProperties.ts | 4 ++-- src/client/views/nodes/button/FontIconBox.tsx | 2 +- 4 files changed, 12 insertions(+), 4 deletions(-) (limited to 'src') diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 9d06ad8a3..656ba61e8 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -1001,6 +1001,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..99829139f 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"; @@ -489,12 +489,19 @@ export class GestureOverlay extends Touchable { e.stopPropagation(); } + @action + checkInkIntersection = (point: PointData) => { + + } + @action onPointerDown = (e: React.PointerEvent) => { if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) { this._points.push({ X: e.clientX, Y: e.clientY }); setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); // if (CurrentUserUtils.SelectedTool === InkTool.Highlighter) SetActiveInkColor("rgba(245, 230, 95, 0.75)"); + } else if (InteractionUtils.IsType(e, InteractionUtils.ERASERTYPE)) { + this.checkInkIntersection({ X: e.clientX, Y: e.clientY }); } } diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 7c91a3323..02288bbb5 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -239,12 +239,12 @@ export class InkStrokeProperties { if (ink) { const screenDragPt = inkView.ComponentView?.ptToScreen?.(ink[controlIndex]); if (screenDragPt) { - var snapData = this.snapToAllCurves(screenDragPt, inkView, { nearestPt: { X: 0, Y: 0 }, distance: 10 }, ink, controlIndex); + const snapData = this.snapToAllCurves(screenDragPt, inkView, { nearestPt: { X: 0, Y: 0 }, distance: 10 }, ink, controlIndex); if (snapData.distance < 10) { 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/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 { -- cgit v1.2.3-70-g09d2 From f6775da5d14f35be2d05de13008f86fbae2cbf68 Mon Sep 17 00:00:00 2001 From: Lauren Date: Fri, 29 Oct 2021 13:57:37 -0400 Subject: working on ink stroke intersection --- .../collectionFreeForm/CollectionFreeFormView.tsx | 27 +++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index febccbfcc..f0064d15e 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 } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { ObjectField } from "../../../../fields/ObjectField"; import { RichTextField } from "../../../../fields/RichTextField"; @@ -51,6 +51,7 @@ import "./CollectionFreeFormView.scss"; import { MarqueeView } from "./MarqueeView"; import React = require("react"); import { ColorScheme } from "../../../util/SettingsManager"; +import { Bezier } from "bezier-js"; export const panZoomSchema = createSchema({ _panX: "number", @@ -111,6 +112,8 @@ export class CollectionFreeFormView extends CollectionSubView(); @observable _marqueeRef = React.createRef(); @observable _keyframeEditing = false; @@ -450,6 +453,11 @@ export class CollectionFreeFormView extends CollectionSubView { if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { + this._prevPoint = this._currPoint = { X: -1, Y: -1 }; document.removeEventListener("pointermove", this.onPointerMove); document.removeEventListener("pointerup", this.onPointerUp); this.removeMoveListeners(); @@ -627,6 +636,10 @@ export class CollectionFreeFormView extends CollectionSubView { 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 (InteractionUtils.IsType(e, InteractionUtils.ERASERTYPE)) { + this._prevPoint = this._currPoint; + this._currPoint = { X: e.clientX, Y: e.clientY } + } if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { if (this.props.isContentActive(true)) e.stopPropagation(); } else if (!e.cancelBubble) { @@ -642,6 +655,18 @@ export class CollectionFreeFormView extends CollectionSubView { + // this.children.filter((doc: Doc) => doc.type === DocumentType.INK) + // .forEach((doc: Doc) => { + // const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView); + // const ctrlPoints = inkView?.ComponentView?.inkScaledData().inkData.slice(); + // const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; + // const intersects = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).intersects({p1: {x: this._prevPoint.X, y: this._prevPoint.Y}, p2: {x: this._currPoint.X, y: this._currPoint.Y}}); + // if (intersects) return inkView; + // }); + return null; + } + handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent) => { if (!e.cancelBubble) { const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); -- cgit v1.2.3-70-g09d2 From 2e6c1f2c1710548ebf86476e1476c22889f11819 Mon Sep 17 00:00:00 2001 From: Lauren Date: Fri, 5 Nov 2021 18:27:07 -0400 Subject: ink document iteration not working --- src/client/views/InkingStroke.tsx | 2 +- .../collectionFreeForm/CollectionFreeFormView.tsx | 52 +++++++++++++++------- 2 files changed, 36 insertions(+), 18 deletions(-) (limited to 'src') diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 81a888256..ecb46a5b3 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -130,7 +130,7 @@ export class InkingStroke extends ViewBoxBaseComponent { diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index f0064d15e..0941db5f8 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -114,6 +114,7 @@ export class CollectionFreeFormView extends CollectionSubView(); @observable _marqueeRef = React.createRef(); @observable _keyframeEditing = false; @@ -453,11 +454,7 @@ export class CollectionFreeFormView extends CollectionSubView { if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { - this._prevPoint = this._currPoint = { X: -1, Y: -1 }; 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.props.Document); + }); + this._prevPoint = this._currPoint = { X: -1, Y: -1 }; + this._deleteList = []; + } } } @@ -636,9 +639,15 @@ export class CollectionFreeFormView extends CollectionSubView { 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 (InteractionUtils.IsType(e, InteractionUtils.ERASERTYPE)) { - this._prevPoint = this._currPoint; - this._currPoint = { X: e.clientX, Y: e.clientY } + if (CurrentUserUtils.SelectedTool !== InkTool.None) { + this._currPoint = { X: e.clientX, Y: e.clientY }; + const intersectStroke = this.getInkIntersection(); + if (intersectStroke) { + if (!this._deleteList.includes(intersectStroke)) { + this._deleteList.push(intersectStroke); + // lower intersectStroke opacity to give user a visual indicator + } + } } if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { if (this.props.isContentActive(true)) e.stopPropagation(); @@ -655,15 +664,24 @@ export class CollectionFreeFormView extends CollectionSubView { - // this.children.filter((doc: Doc) => doc.type === DocumentType.INK) - // .forEach((doc: Doc) => { - // const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView); - // const ctrlPoints = inkView?.ComponentView?.inkScaledData().inkData.slice(); - // const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; - // const intersects = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).intersects({p1: {x: this._prevPoint.X, y: this._prevPoint.Y}, p2: {x: this._currPoint.X, y: this._currPoint.Y}}); - // if (intersects) return inkView; - // }); + this.props.childDocuments?.filter(doc => doc.type === DocumentType.INK) + .forEach(doc => { + console.log("in for each"); + const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView); + const ctrlPoints = Cast(inkView?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? []; + for (var i = 0; i < ctrlPoints.length - 3; i += 4) { + const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; + const intersects = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).intersects( + { p1: { x: this._prevPoint.X, y: this._prevPoint.Y }, + p2: { x: this._currPoint.X, y: this._currPoint.Y } }); + if (intersects) return inkView; + } + }); return null; } -- cgit v1.2.3-70-g09d2 From 50f8a862bd27c212b13fcd1f626aafa5434e4842 Mon Sep 17 00:00:00 2001 From: Lauren Date: Sat, 6 Nov 2021 15:32:47 -0400 Subject: from last --- .../views/collections/collectionFreeForm/CollectionFreeFormView.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) (limited to 'src') diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 0941db5f8..0b7bf92cc 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -669,7 +669,10 @@ export class CollectionFreeFormView extends CollectionSubView { - this.props.childDocuments?.filter(doc => doc.type === DocumentType.INK) + console.log("in intersection"); + const currentCollection = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; + currentCollection?.childDocs + .filter(doc => doc.type === DocumentType.INK) .forEach(doc => { console.log("in for each"); const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView); -- cgit v1.2.3-70-g09d2 From b2b95106d21f03a0b2b46f972a36bbf00068ab20 Mon Sep 17 00:00:00 2001 From: Lauren Date: Sat, 6 Nov 2021 16:49:36 -0400 Subject: eraser tool working --- .../collectionFreeForm/CollectionFreeFormView.tsx | 46 +++++++++++++--------- 1 file changed, 27 insertions(+), 19 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 0b7bf92cc..f60af6355 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -605,9 +605,11 @@ export class CollectionFreeFormView extends CollectionSubView { - ink.props.removeDocument?.(ink.props.Document); - }); + if (this._deleteList.length > 0) { + this._deleteList.forEach(ink => { + ink.props.removeDocument?.(ink.props.Document); + }); + } this._prevPoint = this._currPoint = { X: -1, Y: -1 }; this._deleteList = []; } @@ -641,12 +643,15 @@ export class CollectionFreeFormView extends CollectionSubView 0) { + intersections.forEach(ink =>{ + if (!this._deleteList.includes(ink)) { + this._deleteList.push(ink); + // Lowering ink opacity to give the user a visual indicator of deletion. + ink.Document.opacity = 0.5; + } + }); } } if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { @@ -668,24 +673,27 @@ export class CollectionFreeFormView extends CollectionSubView { - console.log("in intersection"); - const currentCollection = this.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; - currentCollection?.childDocs + getInkIntersection = (): DocumentView[] => { + const inks: DocumentView[] = []; + this.childDocs .filter(doc => doc.type === DocumentType.INK) .forEach(doc => { - console.log("in for each"); const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView); const ctrlPoints = Cast(inkView?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? []; for (var i = 0; i < ctrlPoints.length - 3; i += 4) { const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; - const intersects = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).intersects( - { p1: { x: this._prevPoint.X, y: this._prevPoint.Y }, - p2: { x: this._currPoint.X, y: this._currPoint.Y } }); - if (intersects) return inkView; + // Converting from screen space to ink space for the intersection. + const prevPointInkSpace = inkView?.ComponentView?.ptFromScreen?.(this._prevPoint); + const currPointInkSpace = inkView?.ComponentView?.ptFromScreen?.(this._currPoint); + if (prevPointInkSpace && currPointInkSpace) { + const intersects = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).intersects( + { p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, + p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y } }); + if (inkView && intersects.length > 0) inks.push(inkView); + } } }); - return null; + return inks; } handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent) => { -- cgit v1.2.3-70-g09d2 From f450c8760c0563cdadd414f169902faeace585ec Mon Sep 17 00:00:00 2001 From: vkalev Date: Tue, 9 Nov 2021 16:08:01 -0500 Subject: working on undo/redo --- src/client/views/InkStrokeProperties.ts | 20 +++++++++++--------- .../collectionFreeForm/CollectionFreeFormView.tsx | 8 +++++++- 2 files changed, 18 insertions(+), 10 deletions(-) (limited to 'src') diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 02288bbb5..58d2992e0 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -356,16 +356,18 @@ export class InkStrokeProperties { const oldHandlePoint = ink[handleIndex]; const oppositeHandlePoint = ink[oppositeHandleIndex]; const controlPoint = ink[controlIndex]; - const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale }; const inkCopy = ink.slice(); - inkCopy[handleIndex] = newHandlePoint; - const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); - const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1; - // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle). - if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && - (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) { - const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); - inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); + if (!Number.isNaN(deltaX / xScale) && !Number.isNaN(deltaY / yScale)) { + const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale }; + inkCopy[handleIndex] = newHandlePoint; + const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); + const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1; + // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle). + if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && + (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) { + const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); + inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); + } } return inkCopy; }) diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index f60af6355..018f4f79a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -29,7 +29,7 @@ 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"; @@ -99,6 +99,7 @@ export class CollectionFreeFormView extends CollectionSubView(); private _cachedPool: Map = 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"; } @@ -449,6 +450,7 @@ export class CollectionFreeFormView extends CollectionSubView { if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { document.removeEventListener("pointermove", this.onPointerMove); @@ -607,11 +611,13 @@ export class CollectionFreeFormView extends CollectionSubView 0) { this._deleteList.forEach(ink => { + ink.Document.opacity = 1; ink.props.removeDocument?.(ink.props.Document); }); } this._prevPoint = this._currPoint = { X: -1, Y: -1 }; this._deleteList = []; + this._batch?.end(); } } } -- cgit v1.2.3-70-g09d2 From 44c3ed6af9516c7bb91785934a997c7ab054ec5f Mon Sep 17 00:00:00 2001 From: vkalev Date: Fri, 12 Nov 2021 16:54:51 -0500 Subject: undo works with ink deletion --- package-lock.json | 6 +++--- .../collections/collectionFreeForm/CollectionFreeFormView.tsx | 10 ++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) (limited to 'src') diff --git a/package-lock.json b/package-lock.json index d03f8af6b..128d6fba8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7709,14 +7709,14 @@ "resolved": "https://registry.npmjs.org/image-size-stream/-/image-size-stream-1.1.0.tgz", "integrity": "sha1-Ivou2mbG31AQh0bacUkmSy0l+Gs=", "requires": { - "image-size": "image-size@git+https://github.com/netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1", + "image-size": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1", "readable-stream": "^1.0.33", "tryit": "^1.0.1" }, "dependencies": { "image-size": { - "version": "git+ssh://git@github.com/netroy/image-size.git#da2c863807a3e9602617bdd357b0de3ab4a064c1", - "from": "image-size@git+https://github.com/netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1" + "version": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1", + "from": "github:netroy/image-size#da2c863807a3e9602617bdd357b0de3ab4a064c1" }, "isarray": { "version": "0.0.1", diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 018f4f79a..bd72e4af9 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -99,7 +99,6 @@ export class CollectionFreeFormView extends CollectionSubView(); private _cachedPool: Map = 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"; } @@ -450,7 +449,7 @@ export class CollectionFreeFormView extends CollectionSubView { if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { @@ -610,14 +608,18 @@ export class CollectionFreeFormView extends CollectionSubView 0) { + // Ensuring ink opacity returns to normal if the user undos after deletion. this._deleteList.forEach(ink => { ink.Document.opacity = 1; + }); + const batch = UndoManager.StartBatch("collectionErase"); + this._deleteList.forEach(ink => { ink.props.removeDocument?.(ink.props.Document); }); + batch.end(); } this._prevPoint = this._currPoint = { X: -1, Y: -1 }; this._deleteList = []; - this._batch?.end(); } } } -- cgit v1.2.3-70-g09d2 From 1fd8d25496d6a52902731cddc21969fe10cb2efa Mon Sep 17 00:00:00 2001 From: vkalev Date: Fri, 12 Nov 2021 18:01:42 -0500 Subject: reverting NaN check when moving tangent handle --- src/client/views/InkStrokeProperties.ts | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) (limited to 'src') diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 58d2992e0..02288bbb5 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -356,18 +356,16 @@ export class InkStrokeProperties { const oldHandlePoint = ink[handleIndex]; const oppositeHandlePoint = ink[oppositeHandleIndex]; const controlPoint = ink[controlIndex]; + const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale }; const inkCopy = ink.slice(); - if (!Number.isNaN(deltaX / xScale) && !Number.isNaN(deltaY / yScale)) { - const newHandlePoint = { X: ink[handleIndex].X - deltaX / xScale, Y: ink[handleIndex].Y - deltaY / yScale }; - inkCopy[handleIndex] = newHandlePoint; - const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); - const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1; - // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle). - if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && - (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) { - const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); - inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); - } + inkCopy[handleIndex] = newHandlePoint; + const brokenIndices = Cast(doc.brokenInkIndices, listSpec("number")); + const equivIndex = closed ? (controlIndex === 0 ? ink.length - 1 : controlIndex === ink.length - 1 ? 0 : -1) : -1; + // Rotate opposite handle if user hasn't held 'Alt' key or not first/final control (which have only 1 handle). + if ((!brokenIndices || (!brokenIndices?.includes(controlIndex) && !brokenIndices?.includes(equivIndex))) && + (closed || (handleIndex !== 1 && handleIndex !== ink.length - 2))) { + const angle = this.angleChange(oldHandlePoint, newHandlePoint, controlPoint); + inkCopy[oppositeHandleIndex] = this.rotatePoint(oppositeHandlePoint, controlPoint, angle); } return inkCopy; }) -- cgit v1.2.3-70-g09d2 From 9b5c863f7b88db579fba3ea8f85c43c2562f4289 Mon Sep 17 00:00:00 2001 From: vkalev Date: Sat, 20 Nov 2021 14:11:50 -0500 Subject: fixing undo batch --- .../collectionFreeForm/CollectionFreeFormView.tsx | 26 ++++++++++------------ 1 file changed, 12 insertions(+), 14 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index bd72e4af9..4447b7624 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -99,6 +99,7 @@ export class CollectionFreeFormView extends CollectionSubView(); private _cachedPool: Map = 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"; } @@ -449,12 +450,12 @@ export class CollectionFreeFormView extends CollectionSubView 0) { - // Ensuring ink opacity returns to normal if the user undos after deletion. - this._deleteList.forEach(ink => { - ink.Document.opacity = 1; - }); - const batch = UndoManager.StartBatch("collectionErase"); this._deleteList.forEach(ink => { ink.props.removeDocument?.(ink.props.Document); }); - batch.end(); } this._prevPoint = this._currPoint = { X: -1, Y: -1 }; this._deleteList = []; + this._batch?.end(); } } } @@ -653,7 +649,7 @@ export class CollectionFreeFormView extends CollectionSubView 0) { - intersections.forEach(ink =>{ + intersections.forEach(ink => { if (!this._deleteList.includes(ink)) { this._deleteList.push(ink); // Lowering ink opacity to give the user a visual indicator of deletion. @@ -695,8 +691,10 @@ export class CollectionFreeFormView extends CollectionSubView ({ x: p.X, y: p.Y }))).intersects( - { p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, - p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y } }); + { + p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, + p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y } + }); if (inkView && intersects.length > 0) inks.push(inkView); } } @@ -878,10 +876,10 @@ export class CollectionFreeFormView extends CollectionSubView pos && size).map(({ pos, size }) => ({ pos: pos!, size: size! })); if (measuredDocs.length) { const ranges = measuredDocs.reduce(({ xrange, yrange }, { pos, size }) => // computes range of content - ({ - xrange: { min: Math.min(xrange.min, pos.x), max: Math.max(xrange.max, pos.x + (size.width || 0)) }, - yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) } - }) + ({ + xrange: { min: Math.min(xrange.min, pos.x), max: Math.max(xrange.max, pos.x + (size.width || 0)) }, + yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) } + }) , { xrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE }, yrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE } -- cgit v1.2.3-70-g09d2 From 7b3e1614fa645014c7de69a0f2057e389074c0ed Mon Sep 17 00:00:00 2001 From: vkalev Date: Sat, 20 Nov 2021 15:38:54 -0500 Subject: updating intersections to map t-values to inks --- .../collectionFreeForm/CollectionFreeFormView.tsx | 38 +++++++++++++--------- 1 file changed, 23 insertions(+), 15 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4447b7624..6f79c5eab 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -647,15 +647,18 @@ export class CollectionFreeFormView extends CollectionSubView 0) { - intersections.forEach(ink => { - if (!this._deleteList.includes(ink)) { - this._deleteList.push(ink); - // Lowering ink opacity to give the user a visual indicator of deletion. - ink.Document.opacity = 0.5; - } - }); + const intersections: { [id: number]: DocumentView[] } = this.getEraserIntersections(); + if (intersections) { + for (const t in intersections) { + const inks = intersections[t]; + inks.forEach(ink => { + if (!this._deleteList.includes(ink)) { + this._deleteList.push(ink); + // Lowering ink opacity to give the user a visual indicator of deletion. + ink.Document.opacity = 0.5; + } + }); + } } } if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { @@ -675,10 +678,10 @@ export class CollectionFreeFormView extends CollectionSubView { - const inks: DocumentView[] = []; + getEraserIntersections = (): { [id: number]: DocumentView[] } => { + const intersections: { [id: number]: DocumentView[] } = {}; this.childDocs .filter(doc => doc.type === DocumentType.INK) .forEach(doc => { @@ -690,16 +693,21 @@ export class CollectionFreeFormView extends CollectionSubView ({ x: p.X, y: p.Y }))).intersects( + const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))); + const t = curve.intersects( { p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y } }); - if (inkView && intersects.length > 0) inks.push(inkView); + if (inkView && t) { + t.forEach(val => { + intersections[+t[0]] ? intersections[+t[0]].push(inkView) : intersections[+t[0]] = [inkView]; + }); + } } } }); - return inks; + return intersections; } handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent) => { -- cgit v1.2.3-70-g09d2 From cc5cdf6882e25826ae00220927d31201a3bb997e Mon Sep 17 00:00:00 2001 From: vkalev Date: Sun, 21 Nov 2021 19:37:51 -0500 Subject: splitting/deletion occurs but too many t-values are stored --- src/client/views/GestureOverlay.tsx | 7 -- .../collectionFreeForm/CollectionFreeFormView.tsx | 136 +++++++++++++++++---- src/fields/InkField.ts | 6 + 3 files changed, 117 insertions(+), 32 deletions(-) (limited to 'src') diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index 99829139f..e2193c9ac 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -489,19 +489,12 @@ export class GestureOverlay extends Touchable { e.stopPropagation(); } - @action - checkInkIntersection = (point: PointData) => { - - } - @action onPointerDown = (e: React.PointerEvent) => { if (InteractionUtils.IsType(e, InteractionUtils.PENTYPE) || [InkTool.Highlighter, InkTool.Pen].includes(CurrentUserUtils.SelectedTool)) { this._points.push({ X: e.clientX, Y: e.clientY }); setupMoveUpEvents(this, e, this.onPointerMove, this.onPointerUp, emptyFunction); // if (CurrentUserUtils.SelectedTool === InkTool.Highlighter) SetActiveInkColor("rgba(245, 230, 95, 0.75)"); - } else if (InteractionUtils.IsType(e, InteractionUtils.ERASERTYPE)) { - this.checkInkIntersection({ X: e.clientX, Y: e.clientY }); } } diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 6f79c5eab..ce4acb7ad 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, PointData } from "../../../../fields/InkField"; +import { InkData, InkField, InkTool, PointData, Intersection } from "../../../../fields/InkField"; import { List } from "../../../../fields/List"; import { ObjectField } from "../../../../fields/ObjectField"; import { RichTextField } from "../../../../fields/RichTextField"; @@ -52,6 +52,7 @@ import { MarqueeView } from "./MarqueeView"; import React = require("react"); import { ColorScheme } from "../../../util/SettingsManager"; import { Bezier } from "bezier-js"; +import { GestureOverlay } from "../../GestureOverlay"; export const panZoomSchema = createSchema({ _panX: "number", @@ -647,19 +648,8 @@ export class CollectionFreeFormView extends CollectionSubView { - if (!this._deleteList.includes(ink)) { - this._deleteList.push(ink); - // Lowering ink opacity to give the user a visual indicator of deletion. - ink.Document.opacity = 0.5; - } - }); - } - } + const eraserIntersections: Intersection[] = this.getEraserIntersections(); + if (eraserIntersections) this.eraseInkStrokes(eraserIntersections); } if (InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE)) { if (this.props.isContentActive(true)) e.stopPropagation(); @@ -676,12 +666,79 @@ export class CollectionFreeFormView extends CollectionSubView { + eraserIntersections.forEach(intersect => { + var t1, t2; + var distT1: number, distT2: number; + distT1 = distT2 = Number.MAX_SAFE_INTEGER; + const t = intersect.t; + const ink = intersect.ink; + const ctrlPoints = Cast(ink?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? []; + for (var i = 0; i < ctrlPoints.length - 3; i += 4) { + const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; + const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))); + const inkIntersections: number[] = this.getInkIntersections(curve); + inkIntersections.forEach(currentT => { + const diff = t - currentT; + if (diff > 0 && diff < distT1) { + distT1 = diff; + t1 = currentT; + } else if (diff < 0 && diff < distT2) { + distT2 = -diff; + t2 = currentT; + } + }); + + // Normal case --> deletion of entire stroke + if (!t1 && !t2) { + if (!this._deleteList.includes(ink)) { + this._deleteList.push(ink); + // Lowering ink opacity to give the user a visual indicator of deletion. + ink.Document.opacity = 0.5; + } + } else if (t1 && !t2) { + const splitCurve = curve.split(t1); + const leftInkData: PointData[] = splitCurve.left.points.map(p => ({ X: p.x, Y: p.y })); + GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, leftInkData); + if (!this._deleteList.includes(ink)) { + this._deleteList.push(ink); + // Lowering ink opacity to give the user a visual indicator of deletion. + ink.Document.opacity = 0.5; + } + } else if (!t1 && t2) { + const splitCurve = curve.split(t2); + const rightInkData: PointData[] = splitCurve.right.points.map(p => ({ X: p.x, Y: p.y })); + GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, rightInkData); + if (!this._deleteList.includes(ink)) { + this._deleteList.push(ink); + // Lowering ink opacity to give the user a visual indicator of deletion. + ink.Document.opacity = 0.5; + } + } else if (t1 && t2) { + const splitCurve1 = curve.split(t1); + const leftInkData: PointData[] = splitCurve1.left.points.map(p => ({ X: p.x, Y: p.y })); + GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, leftInkData); + const splitCurve2 = curve.split(t2); + const rightInkData: PointData[] = splitCurve2.right.points.map(p => ({ X: p.x, Y: p.y })); + GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, rightInkData); + + if (!this._deleteList.includes(ink)) { + this._deleteList.push(ink); + // Lowering ink opacity to give the user a visual indicator of deletion. + ink.Document.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 a list of ink DocumentViews. + * @returns A dictionary mapping the t-value intersection of the eraser with the corresponding ink DocumentView. */ - getEraserIntersections = (): { [id: number]: DocumentView[] } => { - const intersections: { [id: number]: DocumentView[] } = {}; + getEraserIntersections = (): Intersection[] => { + const intersections: Intersection[] = []; this.childDocs .filter(doc => doc.type === DocumentType.INK) .forEach(doc => { @@ -694,14 +751,13 @@ export class CollectionFreeFormView extends CollectionSubView ({ x: p.X, y: p.Y }))); - const t = curve.intersects( - { - p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, - p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y } - }); - if (inkView && t) { - t.forEach(val => { - intersections[+t[0]] ? intersections[+t[0]].push(inkView) : intersections[+t[0]] = [inkView]; + const intersects = curve.intersects({ + p1: { x: prevPointInkSpace.X, y: prevPointInkSpace.Y }, + p2: { x: currPointInkSpace.X, y: currPointInkSpace.Y } + }); + if (inkView && intersects) { + intersects.forEach(t => { + intersections.push({ t: +t.toString(), ink: inkView }); }); } } @@ -710,6 +766,36 @@ export class CollectionFreeFormView extends CollectionSubView { + const intersections: number[] = []; + const selfIntersect = curve.selfintersects(); + if (selfIntersect) { + selfIntersect.forEach(t => { + intersections.push(+t.toString()); + }); + } + + this.childDocs + .filter(doc => doc.type === DocumentType.INK) + .forEach(doc => { + const currInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView); + const currCtrls = Cast(currInk?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? []; + for (var i = 0; i < currCtrls.length - 3; i += 4) { + const currArray = [currCtrls[i], currCtrls[i + 1], currCtrls[i + 2], currCtrls[i + 3]]; + const currCurve = new Bezier(currArray.map(p => ({ x: p.X, y: p.Y }))); + const intersect = curve.intersects(currCurve); + if (intersect) { + intersect.forEach(t => { + intersections.push(+this.parseTValue(t.toString())); + }); + } + } + }); + return intersections; + } + + parseTValue = (t: string) => t.split("/", 1)[0]; + handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent) => { if (!e.cancelBubble) { const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index f16e143d8..1d50b5e0d 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -3,6 +3,7 @@ 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"; // Helps keep track of the current ink tool in use. export enum InkTool { @@ -20,6 +21,11 @@ export interface PointData { Y: number; } +export interface Intersection { + t: number; + ink: DocumentView; +} + // Defines an ink as an array of points. export type InkData = Array; -- cgit v1.2.3-70-g09d2 From cc7c677399d1aafa4078741922ffcd0b81ad720f Mon Sep 17 00:00:00 2001 From: vkalev Date: Wed, 24 Nov 2021 12:45:50 -0500 Subject: segmentation of ink strokes prior to deletion --- .../collectionFreeForm/CollectionFreeFormView.tsx | 192 ++++++++++++--------- src/fields/InkField.ts | 9 +- 2 files changed, 121 insertions(+), 80 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index ce4acb7ad..4aac49469 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, PointData, Intersection } 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"; @@ -53,6 +53,7 @@ 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", @@ -648,6 +649,7 @@ export class CollectionFreeFormView extends CollectionSubView { eraserIntersections.forEach(intersect => { - var t1, t2; - var distT1: number, distT2: number; - distT1 = distT2 = Number.MAX_SAFE_INTEGER; - const t = intersect.t; const ink = intersect.ink; - const ctrlPoints = Cast(ink?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? []; - for (var i = 0; i < ctrlPoints.length - 3; i += 4) { - const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; - const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))); - const inkIntersections: number[] = this.getInkIntersections(curve); - inkIntersections.forEach(currentT => { - const diff = t - currentT; - if (diff > 0 && diff < distT1) { - distT1 = diff; - t1 = currentT; - } else if (diff < 0 && diff < distT2) { - distT2 = -diff; - t2 = currentT; - } - }); - - // Normal case --> deletion of entire stroke - if (!t1 && !t2) { - if (!this._deleteList.includes(ink)) { - this._deleteList.push(ink); - // Lowering ink opacity to give the user a visual indicator of deletion. - ink.Document.opacity = 0.5; - } - } else if (t1 && !t2) { - const splitCurve = curve.split(t1); - const leftInkData: PointData[] = splitCurve.left.points.map(p => ({ X: p.x, Y: p.y })); - GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, leftInkData); - if (!this._deleteList.includes(ink)) { - this._deleteList.push(ink); - // Lowering ink opacity to give the user a visual indicator of deletion. - ink.Document.opacity = 0.5; - } - } else if (!t1 && t2) { - const splitCurve = curve.split(t2); - const rightInkData: PointData[] = splitCurve.right.points.map(p => ({ X: p.x, Y: p.y })); - GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, rightInkData); - if (!this._deleteList.includes(ink)) { - this._deleteList.push(ink); - // Lowering ink opacity to give the user a visual indicator of deletion. - ink.Document.opacity = 0.5; - } - } else if (t1 && t2) { - const splitCurve1 = curve.split(t1); - const leftInkData: PointData[] = splitCurve1.left.points.map(p => ({ X: p.x, Y: p.y })); - GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, leftInkData); - const splitCurve2 = curve.split(t2); - const rightInkData: PointData[] = splitCurve2.right.points.map(p => ({ X: p.x, Y: p.y })); - GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, rightInkData); - - if (!this._deleteList.includes(ink)) { - this._deleteList.push(ink); - // Lowering ink opacity to give the user a visual indicator of deletion. - ink.Document.opacity = 0.5; - } + if (ink) { + if (!this._deleteList.includes(ink)) { + const segments: Segment[] = this.segmentInkStroke(ink, intersect.index ?? 0); + segments.forEach(segment => { + const newInkData: PointData[] = []; + // Appending all curves of the current segment together in order to render a single new stroke. + segment.forEach(curve => { + newInkData.push(...curve.points.map(p => ({ X: p.x, Y: p.y }))); + }); + GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, newInkData); + }); + this._deleteList.push(ink); + // Lowering ink opacity to give the user a visual indicator of deletion. + ink.Document.opacity = 0.5; } } }); @@ -756,9 +718,19 @@ export class CollectionFreeFormView extends CollectionSubView { - intersections.push({ t: +t.toString(), ink: inkView }); - }); + for (const val of intersects) { + // Casting t-value from type: (string | number) to number for comparisons. + const t = +val.toString(); + 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, index: i }); + } } } } @@ -766,36 +738,100 @@ export class CollectionFreeFormView extends CollectionSubView { - const intersections: number[] = []; - const selfIntersect = curve.selfintersects(); - if (selfIntersect) { - selfIntersect.forEach(t => { - intersections.push(+t.toString()); - }); + /** + * 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 excludeCurve 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, excludeCurve: number): Segment[] => { + const segments: Segment[] = []; + var segment: Segment = []; + var excludeSegment: boolean = false; + const ctrlPoints = Cast(ink?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? []; + // Iterating through all of the curves of the intersected ink stroke. + for (var i = 0; i < ctrlPoints.length - 3; i += 4) { + // Performing "deletion" of a segment by excluding the segment in which intersection with the eraser occurred. + if (i === excludeCurve) excludeSegment = true; + const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; + const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))); + // Getting all t-value intersections of the current curve with all other curves. + const tVals: number[] = this.getInkIntersections(i, ink, curve); + if (tVals.length > 0) { + tVals.sort(); + const curveCopy = new Bezier(curve.points); + // Splitting only by the first t-value. + const pair = curveCopy.split(tVals[0]); + segment.push(pair.left); + if (!excludeSegment) { + segments.push(segment); + segment = [pair.right]; + excludeSegment = false; + } + // Splitting by the number of t-values returned. + // for (var index = 0; index < tVals.length; index++) { + // const curveCopy = new Bezier(curve.points); + // if (index === 0) { + // const pair = curveCopy.split(tVals[index]); + // segment.push(pair.left); + // segments.push(segment); + // segment = [pair.right]; + // } else if (index === tVals.length - 1) { + // segments.push(segment); + // const pair = curveCopy.split(tVals[index]); + // segment = [pair.right]; + // } else { + // segments.push(segment); + // const curve = curveCopy.split(index, index + 1); + // segment = [curve]; + // } + // } + } else { + segment.push(curve); + } } + if (!excludeSegment) 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 currInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView); const currCtrls = Cast(currInk?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? []; - for (var i = 0; i < currCtrls.length - 3; i += 4) { - const currArray = [currCtrls[i], currCtrls[i + 1], currCtrls[i + 2], currCtrls[i + 3]]; + for (var j = 0; j < currCtrls.length - 3; j += 4) { + const exclude = i === j || i === j - 4 || i === j + 4; + const sameDoc = ink?.Document === currInk?.Document; + // Ensuring that the curve intersected by the eraser is not checked for further ink intersections. + if (sameDoc && exclude) continue; + const currArray = [currCtrls[j], currCtrls[j + 1], currCtrls[j + 2], currCtrls[j + 3]]; const currCurve = new Bezier(currArray.map(p => ({ x: p.X, y: p.Y }))); const intersect = curve.intersects(currCurve); - if (intersect) { - intersect.forEach(t => { - intersections.push(+this.parseTValue(t.toString())); + if (intersect.length > 0) { + intersect.forEach(val => { + // Converting the Bezier.js Split type to a t-value number. + const t = +val.toString().split("/")[0]; + if (!tVals.includes(t)) tVals.push(t); }); } } }); - return intersections; + return tVals; } - parseTValue = (t: string) => t.split("/", 1)[0]; - handle1PointerMove = (e: TouchEvent, me: InteractionUtils.MultiTouchEvent) => { if (!e.cancelBubble) { const myTouches = InteractionUtils.GetMyTargetTouches(me, this.prevPoints, true); diff --git a/src/fields/InkField.ts b/src/fields/InkField.ts index 1d50b5e0d..f61313674 100644 --- a/src/fields/InkField.ts +++ b/src/fields/InkField.ts @@ -4,6 +4,7 @@ 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 { @@ -22,10 +23,14 @@ export interface PointData { } export interface Intersection { - t: number; - ink: DocumentView; + t?: number; + ink?: DocumentView; + curve?: Bezier; + index?: number; } +export type Segment = Array; + // Defines an ink as an array of points. export type InkData = Array; -- cgit v1.2.3-70-g09d2 From 8405ed2e3ebaf7ba7842a5619e9b252bf5eb9c4c Mon Sep 17 00:00:00 2001 From: bobzel Date: Thu, 25 Nov 2021 21:45:06 -0500 Subject: fixed up erasing ink by segments --- .../collectionFreeForm/CollectionFreeFormView.tsx | 156 +++++++++------------ 1 file changed, 65 insertions(+), 91 deletions(-) (limited to 'src') diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 4aac49469..df690da49 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -34,7 +34,7 @@ import { COLLECTION_BORDER_WIDTH } from "../../../views/global/globalCssVariable 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"; @@ -610,11 +610,7 @@ export class CollectionFreeFormView extends CollectionSubView 0) { - this._deleteList.forEach(ink => { - ink.props.removeDocument?.(ink.props.Document); - }); - } + this._deleteList.forEach(ink => ink.props.removeDocument?.(ink.rootDoc)); this._prevPoint = this._currPoint = { X: -1, Y: -1 }; this._deleteList = []; this._batch?.end(); @@ -650,8 +646,7 @@ export class CollectionFreeFormView extends CollectionSubView { eraserIntersections.forEach(intersect => { const ink = intersect.ink; - if (ink) { - if (!this._deleteList.includes(ink)) { - const segments: Segment[] = this.segmentInkStroke(ink, intersect.index ?? 0); - segments.forEach(segment => { - const newInkData: PointData[] = []; - // Appending all curves of the current segment together in order to render a single new stroke. - segment.forEach(curve => { - newInkData.push(...curve.points.map(p => ({ X: p.x, Y: p.y }))); - }); - GestureOverlay.Instance.dispatchGesture(GestureUtils.Gestures.Stroke, newInkData); - }); - this._deleteList.push(ink); - // Lowering ink opacity to give the user a visual indicator of deletion. - ink.Document.opacity = 0.5; - } + 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; } }); } @@ -705,12 +697,13 @@ export class CollectionFreeFormView extends CollectionSubView doc.type === DocumentType.INK) .forEach(doc => { const inkView = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView); - const ctrlPoints = Cast(inkView?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? []; - for (var i = 0; i < ctrlPoints.length - 3; i += 4) { - const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; + 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 = inkView?.ComponentView?.ptFromScreen?.(this._prevPoint); - const currPointInkSpace = inkView?.ComponentView?.ptFromScreen?.(this._currPoint); + 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({ @@ -720,7 +713,7 @@ export class CollectionFreeFormView extends CollectionSubView { + segmentInkStroke = (ink: DocumentView, excludeT: number): Segment[] => { const segments: Segment[] = []; var segment: Segment = []; - var excludeSegment: boolean = false; - const ctrlPoints = Cast(ink?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? []; - // Iterating through all of the curves of the intersected ink stroke. - for (var i = 0; i < ctrlPoints.length - 3; i += 4) { - // Performing "deletion" of a segment by excluding the segment in which intersection with the eraser occurred. - if (i === excludeCurve) excludeSegment = true; - const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; - const curve = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))); + 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: number[] = this.getInkIntersections(i, ink, curve); - if (tVals.length > 0) { - tVals.sort(); - const curveCopy = new Bezier(curve.points); - // Splitting only by the first t-value. - const pair = curveCopy.split(tVals[0]); - segment.push(pair.left); - if (!excludeSegment) { - segments.push(segment); - segment = [pair.right]; - excludeSegment = false; - } - // Splitting by the number of t-values returned. - // for (var index = 0; index < tVals.length; index++) { - // const curveCopy = new Bezier(curve.points); - // if (index === 0) { - // const pair = curveCopy.split(tVals[index]); - // segment.push(pair.left); - // segments.push(segment); - // segment = [pair.right]; - // } else if (index === tVals.length - 1) { - // segments.push(segment); - // const pair = curveCopy.split(tVals[index]); - // segment = [pair.right]; - // } else { - // segments.push(segment); - // const curve = curveCopy.split(index, index + 1); - // segment = [curve]; - // } - // } + 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 (!excludeSegment) segments.push(segment); + if (excludeT < startSegmentT || excludeT > (inkData.length / 4)) { + segment.length && segments.push(segment); + } return segments; } @@ -810,23 +786,21 @@ export class CollectionFreeFormView extends CollectionSubView doc.type === DocumentType.INK) .forEach(doc => { - const currInk = DocumentManager.Instance.getDocumentView(doc, this.props.CollectionView); - const currCtrls = Cast(currInk?.dataDoc[this.props.fieldKey], InkField)?.inkData ?? []; - for (var j = 0; j < currCtrls.length - 3; j += 4) { - const exclude = i === j || i === j - 4 || i === j + 4; - const sameDoc = ink?.Document === currInk?.Document; + 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 (sameDoc && exclude) continue; - const currArray = [currCtrls[j], currCtrls[j + 1], currCtrls[j + 2], currCtrls[j + 3]]; - const currCurve = new Bezier(currArray.map(p => ({ x: p.X, y: p.Y }))); - const intersect = curve.intersects(currCurve); - if (intersect.length > 0) { - intersect.forEach(val => { - // Converting the Bezier.js Split type to a t-value number. - const t = +val.toString().split("/")[0]; - if (!tVals.includes(t)) tVals.push(t); - }); - } + 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; @@ -1006,10 +980,10 @@ export class CollectionFreeFormView extends CollectionSubView pos && size).map(({ pos, size }) => ({ pos: pos!, size: size! })); if (measuredDocs.length) { const ranges = measuredDocs.reduce(({ xrange, yrange }, { pos, size }) => // computes range of content - ({ - xrange: { min: Math.min(xrange.min, pos.x), max: Math.max(xrange.max, pos.x + (size.width || 0)) }, - yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) } - }) + ({ + xrange: { min: Math.min(xrange.min, pos.x), max: Math.max(xrange.max, pos.x + (size.width || 0)) }, + yrange: { min: Math.min(yrange.min, pos.y), max: Math.max(yrange.max, pos.y + (size.height || 0)) } + }) , { xrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE }, yrange: { min: Number.MAX_VALUE, max: -Number.MAX_VALUE } -- cgit v1.2.3-70-g09d2