aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbobzel <zzzman@gmail.com>2021-09-29 15:15:21 -0400
committerbobzel <zzzman@gmail.com>2021-09-29 15:15:21 -0400
commit5f95911a504a47c867198fccc32a75bf22d26056 (patch)
treed98ff4a6243de2d2bc615540db5b040793e82496 /src
parente6451eda7c7a5be73922b302627c53db5e22d474 (diff)
added snapping to close curve or to self-snap a vertex to its curve. fixed ink decorations from being clipped when zoomed. fixed crash with zero-length tangent
Diffstat (limited to 'src')
-rw-r--r--src/client/views/InkControlPtHandles.tsx18
-rw-r--r--src/client/views/InkStrokeProperties.ts61
-rw-r--r--src/client/views/InkingStroke.tsx26
-rw-r--r--src/client/views/MainView.tsx2
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)}