diff options
-rw-r--r-- | src/client/views/InkControlPtHandles.tsx | 18 | ||||
-rw-r--r-- | src/client/views/InkStrokeProperties.ts | 61 | ||||
-rw-r--r-- | src/client/views/InkingStroke.tsx | 26 | ||||
-rw-r--r-- | src/client/views/MainView.tsx | 2 |
4 files changed, 78 insertions, 29 deletions
diff --git a/src/client/views/InkControlPtHandles.tsx b/src/client/views/InkControlPtHandles.tsx index f80aca268..249a0fa64 100644 --- a/src/client/views/InkControlPtHandles.tsx +++ b/src/client/views/InkControlPtHandles.tsx @@ -42,10 +42,13 @@ export class InkControlPtHandles extends React.Component<InkControlProps> { setupMoveUpEvents(this, e, action((e: PointerEvent, down: number[], delta: number[]) => { if (!this.controlUndo) this.controlUndo = UndoManager.StartBatch("drag ink ctrl pt"); - InkStrokeProperties.Instance?.moveControl(-delta[0] * screenScale, -delta[1] * screenScale, controlIndex); + InkStrokeProperties.Instance?.moveControl(delta[0] * screenScale, delta[1] * screenScale, controlIndex); return false; }), action(() => { + if (this.controlUndo) { + InkStrokeProperties.Instance?.snapControl(this.props.inkDoc, controlIndex); + } this.controlUndo?.end(); this.controlUndo = undefined; UndoManager.FilterBatches(["data", "x", "y", "width", "height"]); @@ -117,20 +120,21 @@ export class InkControlPtHandles extends React.Component<InkControlProps> { } const screenSpaceLineWidth = this.props.screenSpaceLineWidth; + const closed = inkData.lastElement().X === inkData[0].X && inkData.lastElement().Y === inkData[0].Y; const nearestScreenPt = this.props.nearestScreenPt(); const TagType = (broken?: boolean) => broken ? "rect" : "circle"; const hdl = (control: { X: number, Y: number, I: number }, scale: number, color: string) => { const broken = Cast(this.props.inkDoc.brokenInkIndices, listSpec("number"))?.includes(control.I); - const Tag = TagType(broken) as keyof JSX.IntrinsicElements; - return <circle key={control.I.toString() + scale} - x={control.X - screenSpaceLineWidth * 3 * scale} - y={control.Y - screenSpaceLineWidth * 3 * scale} + const Tag = TagType((control.I === 0 || control.I === inkData.length - 1) && !closed) as keyof JSX.IntrinsicElements; + return <Tag key={control.I.toString() + scale} + x={control.X - screenSpaceLineWidth * 2 * scale} + y={control.Y - screenSpaceLineWidth * 2 * scale} cx={control.X} cy={control.Y} r={screenSpaceLineWidth * 2 * scale} opacity={this.controlUndo ? 0.15 : 1} - height={screenSpaceLineWidth * 6 * scale} - width={screenSpaceLineWidth * 6 * scale} + height={screenSpaceLineWidth * 4 * scale} + width={screenSpaceLineWidth * 4 * scale} strokeWidth={screenSpaceLineWidth / 2} stroke={Colors.MEDIUM_BLUE} fill={broken ? Colors.MEDIUM_BLUE : color} diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 3770eb7c1..ac5cdfee2 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -189,7 +189,7 @@ export class InkStrokeProperties { const order = controlIndex % 4; const closed = ink.lastElement().X === ink[0].X && ink.lastElement().Y === ink[0].Y; - return ink.map((pt, i) => { + const newpts = ink.map((pt, i) => { const leftHandlePoint = order === 0 && i === controlIndex + 1; const rightHandlePoint = order === 0 && controlIndex !== 0 && i === controlIndex - 2; if (controlIndex === i || @@ -201,12 +201,68 @@ export class InkStrokeProperties { (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 1) || (order === 3 && controlIndex !== ink.length - 1 && i === controlIndex + 2) || ((ink[0].X === ink[ink.length - 1].X) && (ink[0].Y === ink[ink.length - 1].Y) && (i === 0 || i === ink.length - 1) && (controlIndex === 0 || controlIndex === ink.length - 1))) { - return ({ X: pt.X - deltaX / xScale, Y: pt.Y - deltaY / yScale }); + return ({ X: pt.X + deltaX / xScale, Y: pt.Y + deltaY / yScale }); } return pt; }); + return newpts; }) + + public static nearestPtToStroke(ctrlPoints: { X: number, Y: number }[], refPt: { X: number, Y: number }, excludeSegs?: number[]) { + var distance = Number.MAX_SAFE_INTEGER; + var nearestT = -1; + var nearestSeg = -1; + var nearestPt = { X: 0, Y: 0 }; + for (var i = 0; i < ctrlPoints.length - 3; i += 4) { + if (excludeSegs?.includes(i)) continue; + const array = [ctrlPoints[i], ctrlPoints[i + 1], ctrlPoints[i + 2], ctrlPoints[i + 3]]; + const point = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).project({ x: refPt.X, y: refPt.Y }); + if (point.t !== undefined) { + const dist = Math.sqrt((point.x - refPt.X) * (point.x - refPt.X) + (point.y - refPt.Y) * (point.y - refPt.Y)); + if (dist < distance) { + distance = dist; + nearestT = point.t; + nearestSeg = i; + nearestPt = { X: point.x, Y: point.y }; + } + } + } + return { distance, nearestT, nearestSeg, nearestPt }; + } + + /** + * Handles the movement/scaling of a control point. + */ + snapControl = (inkDoc: Doc, controlIndex: number) => { + const ink = Cast(inkDoc.data, InkField)?.inkData; + if (ink) { + const closed = ink.lastElement().X === ink[0].X && ink.lastElement().Y === ink[0].Y; + + // figure out which segments we don't want to snap to - avoid the dragged control point's segment and the next and prev segments (when they exist -- ie not for endpoints of unclosed curve) + const thisseg = Math.floor(controlIndex / 4) * 4; + const which = controlIndex % 4; + const nextseg = which > 1 && (closed || controlIndex < ink.length - 1) ? (thisseg + 4) % ink.length : -1; + const prevseg = which < 2 && (closed || controlIndex > 0) ? (thisseg - 4 + ink.length) % ink.length : -1; + const refPt = ink[controlIndex]; + const { nearestPt } = InkStrokeProperties.nearestPtToStroke(ink, refPt, [thisseg, prevseg, nextseg]); + + // nearestPt is in inkDoc coordinates -- we need to compute the distance in screen coordinates. + // so we scale the X & Y distances by the internal ink scale factor and then transform the final distance by the ScreenToLocal.Scale of the inkDoc itself. + const oldXrange = (xs => ({ coord: NumCast(inkDoc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X)); + const oldYrange = (ys => ({ coord: NumCast(inkDoc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); + const ptsXscale = NumCast(inkDoc._width) / (oldXrange.max - oldXrange.min); + const ptsYscale = NumCast(inkDoc._height) / (oldYrange.max - oldYrange.min); + const near = Math.sqrt((nearestPt.X - refPt.X) * (nearestPt.X - refPt.X) * ptsXscale * ptsXscale + + (nearestPt.Y - refPt.Y) * (nearestPt.Y - refPt.Y) * ptsYscale * ptsYscale); + + if (near / (this.selectedInk?.lastElement().props.ScreenToLocalTransform().Scale || 1) < 10) { + return this.moveControl((nearestPt.X - ink[controlIndex].X) * ptsXscale, (nearestPt.Y - ink[controlIndex].Y) * ptsYscale, controlIndex) + } + } + return false; + } + /** * Snaps a control point with broken tangency back to synced rotation. * @param handleIndexA The handle point that retains its current position. @@ -247,6 +303,7 @@ export class InkStrokeProperties { angleBetweenTwoVectors = (vectorA: PointData, vectorB: PointData) => { const magnitudeA = Math.sqrt(vectorA.X * vectorA.X + vectorA.Y * vectorA.Y); const magnitudeB = Math.sqrt(vectorB.X * vectorB.X + vectorB.Y * vectorB.Y); + if (magnitudeA === 0 || magnitudeB === 0) return 0; // Normalizing the vectors. vectorA = { X: vectorA.X / magnitudeA, Y: vectorA.Y / magnitudeA }; vectorB = { X: vectorB.X / magnitudeB, Y: vectorB.Y / magnitudeB }; diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 4cd7f7698..8b1b3ea32 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -130,25 +130,14 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint( (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] })); - var nearest = Number.MAX_SAFE_INTEGER; - this._nearestT = -1; - this._nearestSeg = -1; - this._nearestScrPt = { X: 0, Y: 0 }; - for (var i = 0; i < screenPts.length - 3; i += 4) { - const array = [screenPts[i], screenPts[i + 1], screenPts[i + 2], screenPts[i + 3]]; - const point = new Bezier(array.map(p => ({ x: p.X, y: p.Y }))).project({ x: e.clientX, y: e.clientY }); - if (point.t) { - const dist = (point.x - e.clientX) * (point.x - e.clientX) + (point.y - e.clientY) * (point.y - e.clientY); - if (dist < nearest) { - nearest = dist; - this._nearestT = point.t; - this._nearestSeg = i; - this._nearestScrPt = { X: point.x, Y: point.y }; - } - } - } + const { distance, nearestT, nearestSeg, nearestPt } = InkStrokeProperties.nearestPtToStroke(screenPts, { X: e.clientX, Y: e.clientY }); + + this._nearestT = nearestT; + this._nearestSeg = nearestSeg; + this._nearestScrPt = nearestPt; } + nearestScreenPt = () => this._nearestScrPt; componentUI = (boundsLeft: number, boundsTop: number) => { const inkDoc = this.props.Document; @@ -159,11 +148,10 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps, InkDocume const screenPts = inkData.map(point => this.props.ScreenToLocalTransform().inverse().transformPoint( (point.X - inkLeft - inkStrokeWidth / 2) * inkScaleX + inkStrokeWidth / 2, (point.Y - inkTop - inkStrokeWidth / 2) * inkScaleY + inkStrokeWidth / 2)).map(p => ({ X: p[0], Y: p[1] })); - const screenOrigin = this.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); const screenHdlPts = screenPts; return <div className="inkstroke-UI" style={{ - clip: `rect(${boundsTop - screenOrigin[1]}px, 10000px, 10000px, ${boundsLeft - screenOrigin[0]}px)` + clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }} > {InteractionUtils.CreatePolyline(screenPts, 0, 0, Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth, StrCast(inkDoc.strokeBezier), StrCast(inkDoc.fillColor, "none"), diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 3f7df705f..6d0d5eb39 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -618,7 +618,7 @@ export class MainView extends React.Component { <GroupManager /> <GoogleAuthenticationManager /> <DocumentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDoc} PanelWidth={this._windowWidth} PanelHeight={this._windowHeight} /> - <ComponentDecorations boundsLeft={0} boundsTop={this.topOfMainDocContent} /> + <ComponentDecorations boundsLeft={this.leftScreenOffsetOfMainDocView} boundsTop={this.topOfMainDocContent} /> {this.topbar} {LinkDescriptionPopup.descriptionPopup ? <LinkDescriptionPopup /> : null} {DocumentLinksButton.LinkEditorDocView ? <LinkMenu docView={DocumentLinksButton.LinkEditorDocView} changeFlyout={emptyFunction} /> : (null)} |