Skip to content

Commit

Permalink
refactor: Extract a function to dedicated file and add test for it
Browse files Browse the repository at this point in the history
  • Loading branch information
miyanokomiya committed Nov 25, 2024
1 parent 233952f commit fc4f396
Show file tree
Hide file tree
Showing 3 changed files with 145 additions and 97 deletions.
44 changes: 44 additions & 0 deletions src/composables/lineAttachmentHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
getLineAttachmentPatch,
getNextAttachmentAnchor,
newPreserveAttachmentHandler,
snapRectWithLineAttachment,
} from "./lineAttachmentHandler";
import { newShapeComposite } from "./shapeComposite";
import { createShape, getCommonStruct } from "../shapes";
Expand All @@ -15,6 +16,7 @@ import { RectangleShape } from "../shapes/rectangle";
import { Shape } from "../models";
import { getLineEdgeInfo } from "../shapes/utils/line";
import { TreeNodeShape } from "../shapes/tree/treeNode";
import { SnappingResult } from "./shapeSnapping";

describe("getLineAttachmentPatch", () => {
const line = createShape<LineShape>(getCommonStruct, "line", { id: "line", q: { x: 100, y: 0 } });
Expand Down Expand Up @@ -577,3 +579,45 @@ describe("newPreserveAttachmentHandler", () => {
});
});
});

describe("snapRectWithLineAttachment", () => {
test("should return line attachment with snapping", () => {
const line = createShape<LineShape>(getCommonStruct, "line", { id: "line", q: { x: 100, y: 100 } });
const snappingResult: SnappingResult = {
diff: { x: 2, y: 0 },
targets: [
{
id: "a",
line: [
{ x: 50, y: 0 },
{ x: 50, y: 100 },
],
},
],
intervalTargets: [],
};
const result0 = snapRectWithLineAttachment({
line,
edgeInfo: getLineEdgeInfo(line),
snappingResult,
movingRect: { x: 48, y: 48, width: 10, height: 10 },
movingRectAnchorRate: 0.58,
movingRectAnchor: { x: 58, y: 58 },
scale: 1,
});
expect(result0?.lineAnchor).toEqualPoint({ x: 60, y: 60 });
expect(result0?.lineAnchorRate).toBeCloseTo(0.6);

const result1 = snapRectWithLineAttachment({
line,
edgeInfo: getLineEdgeInfo(line),
snappingResult: { ...snappingResult, diff: { x: -2, y: 0 } },
movingRect: { x: 42, y: 42, width: 10, height: 10 },
movingRectAnchorRate: 0.42,
movingRectAnchor: { x: 42, y: 42 },
scale: 1,
});
expect(result1?.lineAnchor).toEqualPoint({ x: 40, y: 40 });
expect(result1?.lineAnchorRate).toBeCloseTo(0.4);
});
});
88 changes: 85 additions & 3 deletions src/composables/lineAttachmentHandler.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import { AffineMatrix, clamp, getDistanceSq, getRectCenter, isSame, IVec2, lerpPoint, MINVALUE, rotate } from "okageo";
import {
add,
AffineMatrix,
clamp,
getDistance,
getDistanceSq,
getRectCenter,
IRectangle,
isSame,
IVec2,
lerpPoint,
MINVALUE,
rotate,
sub,
} from "okageo";
import { EntityPatchInfo, Shape, StyleScheme } from "../models";
import { isLineShape, LineShape } from "../shapes/line";
import { getLineEdgeInfo } from "../shapes/utils/line";
import { getClosestPointOnPolyline } from "../utils/path";
import { getIntersectionsBetweenLineShapeAndLine, getLineEdgeInfo } from "../shapes/utils/line";
import { getClosestPointOnPolyline, PolylineEdgeInfo } from "../utils/path";
import { ShapeComposite } from "./shapeComposite";
import { AppCanvasStateContext } from "./states/appCanvas/core";
import {
Expand All @@ -17,15 +31,24 @@ import {
toMap,
} from "../utils/commons";
import {
getClosestLineToRectFeaturePoints,
getD2,
getPointLerpSlope,
getRelativePointWithinRect,
getRelativeRateWithinRect,
getRotateFn,
getRotationAffine,
ISegment,
TAU,
} from "../utils/geometry";
import { getCurveLinePatch } from "./curveLineHandler";
import { applyFillStyle } from "../utils/fillStyle";
import {
filterSnappingTargetsBySecondGuideline,
getSecondGuidelineCandidateInfo,
SNAP_THRESHOLD,
SnappingResult,
} from "./shapeSnapping";

export interface LineAttachmentHandler {
onModified(updatedMap: { [id: string]: Partial<Shape> }): { [id: string]: Partial<Shape> };
Expand Down Expand Up @@ -397,3 +420,62 @@ export function newPreserveAttachmentHandler({

return { setActive, getPatch, render, hasAttachment };
}

export function snapRectWithLineAttachment({
line,
edgeInfo,
snappingResult,
movingRect,
movingRectAnchorRate,
movingRectAnchor,
scale,
}: {
line: LineShape;
edgeInfo: PolylineEdgeInfo;
snappingResult: SnappingResult;
movingRect: IRectangle;
movingRectAnchorRate: number;
movingRectAnchor: IVec2;
scale: number;
}):
| {
snappingResult: SnappingResult;
lineAnchorRate: number;
lineAnchor: IVec2;
}
| undefined {
// Get slope at the latest anchor point on the line.
// Use this slope as first guideline to get second guideline candidates.
const slopeV = rotate({ x: 1, y: 0 }, getPointLerpSlope(edgeInfo.lerpFn, movingRectAnchorRate));
const candidateInfo = getSecondGuidelineCandidateInfo(snappingResult, slopeV);

// Get currently snapped anchor that isn't on the line.
const snappedAnchor = add(movingRectAnchor, snappingResult.diff);
// Get the closest candidate to a feature point of moving rect as second guideline.
const secondGuideline = getClosestLineToRectFeaturePoints(movingRect, candidateInfo.candidates);
if (!secondGuideline) return;

// Slide second guideline to the snapped anchor.
const secondGuidelineAtSnappedAnchor: ISegment = [
snappedAnchor,
add(sub(secondGuideline[1], secondGuideline[0]), snappedAnchor),
];
// Get intersections between the line and adjusted second guideline.
// This intersections are on the line and keep second guideline valid.
const intersections = getIntersectionsBetweenLineShapeAndLine(line, secondGuidelineAtSnappedAnchor);
const nextLineAnchorP = pickMinItem(intersections, (p) => getD2(sub(p, movingRectAnchor)));
if (!nextLineAnchorP || getDistance(nextLineAnchorP, movingRectAnchor) >= SNAP_THRESHOLD * scale) return;

// The anchor point is determined but stlll need to get its rate on the line.
const closestInfo = getClosestPointOnPolyline(edgeInfo, nextLineAnchorP, Infinity);
if (!closestInfo) return;

return {
snappingResult: {
diff: snappingResult.diff,
...filterSnappingTargetsBySecondGuideline(candidateInfo, secondGuideline),
},
lineAnchorRate: closestInfo[1],
lineAnchor: nextLineAnchorP,
};
}
110 changes: 16 additions & 94 deletions src/composables/states/appCanvas/lines/movingOnLineState.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,23 @@
import type { AppCanvasState, AppCanvasStateContext } from "../core";
import { applyFillStyle } from "../../../../utils/fillStyle";
import { mapReduce, patchPipe, pickMinItem, toList, toMap } from "../../../../utils/commons";
import { mapReduce, patchPipe, toList, toMap } from "../../../../utils/commons";
import { getLinePath, isLineShape, LineShape } from "../../../../shapes/line";
import { getIntersectionsBetweenLineShapeAndLine, getLineEdgeInfo } from "../../../../shapes/utils/line";
import {
getClosestLineToRectFeaturePoints,
getD2,
getDiagonalLengthOfRect,
getPointLerpSlope,
ISegment,
TAU,
} from "../../../../utils/geometry";
import { add, getDistance, IRectangle, IVec2, moveRect, rotate, sub } from "okageo";
import { getLineEdgeInfo } from "../../../../shapes/utils/line";
import { getDiagonalLengthOfRect, TAU } from "../../../../utils/geometry";
import { add, getDistance, IRectangle, IVec2, moveRect, sub } from "okageo";
import {
getAttachmentAnchorPoint,
getEvenlySpacedLineAttachment,
getEvenlySpacedLineAttachmentBetweenFixedOnes,
snapRectWithLineAttachment,
} from "../../../lineAttachmentHandler";
import { Shape, ShapeAttachment } from "../../../../models";
import { applyCurvePath } from "../../../../utils/renderer";
import { applyStrokeStyle } from "../../../../utils/strokeStyle";
import { getPatchAfterLayouts } from "../../../shapeLayoutHandler";
import { COMMAND_EXAM_SRC } from "../commandExams";
import { getNextShapeComposite } from "../../../shapeComposite";
import {
filterSnappingTargetsBySecondGuideline,
getSecondGuidelineCandidateInfo,
newShapeSnapping,
renderSnappingResult,
ShapeSnapping,
SNAP_THRESHOLD,
SnappingResult,
} from "../../../shapeSnapping";
import { newShapeSnapping, renderSnappingResult, ShapeSnapping, SnappingResult } from "../../../shapeSnapping";
import { getSnappableCandidates } from "../commons";
import { getClosestPointOnPolyline, PolylineEdgeInfo } from "../../../../utils/path";

Expand Down Expand Up @@ -157,17 +143,19 @@ export function newMovingOnLineState(option: Option): AppCanvasState {
lineAnchor = closestInfo[0];
attachInfoMap = new Map([[option.shapeId, [nextTo]]]);

const scale = ctx.getScale();
const movingRect = moveRect(movingRectAtStart, sub(lineAnchor, anchorPointAtStart));
const shapeSnappingResult = shapeSnapping.test(movingRect, undefined, scale);
snappingResult = undefined;
if (!event.data.ctrl && movingRectAtStart) {
const result = snapPointOnLine({
if (!event.data.ctrl && shapeSnappingResult) {
const result = snapRectWithLineAttachment({
line,
shapeSnapping,
movingRectAtStart,
lineAnchorRate: closestInfo[1],
lineAnchorP: lineAnchor,
anchorPointAtStart,
edgeInfo,
scale: ctx.getScale(),
snappingResult: shapeSnappingResult,
movingRect,
movingRectAnchorRate: closestInfo[1],
movingRectAnchor: lineAnchor,
scale,
});
if (result) {
snappingResult = result.snappingResult;
Expand Down Expand Up @@ -294,69 +282,3 @@ export function newMovingOnLineState(option: Option): AppCanvasState {
},
};
}

function snapPointOnLine({
line,
shapeSnapping,
movingRectAtStart,
lineAnchorRate,
lineAnchorP,
anchorPointAtStart,
edgeInfo,
scale,
}: {
line: LineShape;
shapeSnapping: ShapeSnapping;
movingRectAtStart: IRectangle;
lineAnchorRate: number;
lineAnchorP: IVec2;
anchorPointAtStart: IVec2;
edgeInfo: PolylineEdgeInfo;
scale: number;
}):
| {
snappingResult: SnappingResult;
lineAnchorRate: number;
lineAnchor: IVec2;
}
| undefined {
const anchorDiff = sub(lineAnchorP, anchorPointAtStart);
const movingRect = moveRect(movingRectAtStart, anchorDiff);
const result = shapeSnapping.test(movingRect, undefined, scale);
if (!result) return;

// Get slope at the latest anchor point on the line.
// Use this slope as first guideline to get second guideline candidates.
const slopeV = rotate({ x: 1, y: 0 }, getPointLerpSlope(edgeInfo.lerpFn, lineAnchorRate));
const candidateInfo = getSecondGuidelineCandidateInfo(result, slopeV);

// Get currently snapped anchor that isn't on the line.
const snappedAnchor = add(lineAnchorP, result.diff);
// Get the closest candidate to a feature point of moving rect as second guideline.
const secondGuideline = getClosestLineToRectFeaturePoints(movingRect, candidateInfo.candidates);
if (!secondGuideline) return;

// Slide second guideline to the snapped anchor.
const secondGuidelineAtSnappedAnchor: ISegment = [
snappedAnchor,
add(sub(secondGuideline[1], secondGuideline[0]), snappedAnchor),
];
// Get intersections between the line and adjusted second guideline.
// This intersections are on the line and keep second guideline valid.
const intersections = getIntersectionsBetweenLineShapeAndLine(line, secondGuidelineAtSnappedAnchor);
const nextLineAnchorP = pickMinItem(intersections, (p) => getD2(sub(p, lineAnchorP)));
if (!nextLineAnchorP || getDistance(nextLineAnchorP, lineAnchorP) >= SNAP_THRESHOLD * scale) return;

// The anchor point is determined but stlll need to get its rate on the line.
const closestInfo = getClosestPointOnPolyline(edgeInfo, nextLineAnchorP, Infinity);
if (!closestInfo) return;

return {
snappingResult: {
diff: result.diff,
...filterSnappingTargetsBySecondGuideline(candidateInfo, secondGuideline),
},
lineAnchorRate: closestInfo[1],
lineAnchor: nextLineAnchorP,
};
}

0 comments on commit fc4f396

Please sign in to comment.