Skip to content

Commit

Permalink
feat: Use the bounds of the index shape as snapping source
Browse files Browse the repository at this point in the history
  • Loading branch information
miyanokomiya committed Nov 14, 2024
1 parent 7798f05 commit a7c9776
Show file tree
Hide file tree
Showing 3 changed files with 211 additions and 3 deletions.
137 changes: 136 additions & 1 deletion src/composables/shapeSnapping.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from "vitest";
import { newShapeIntervalSnapping, newShapeSnapping } from "./shapeSnapping";
import { mergetSnappingResult, newShapeIntervalSnapping, newShapeSnapping, SnappingResult } from "./shapeSnapping";
import { ShapeSnappingLines } from "../shapes/core";

describe("newShapeSnapping", () => {
Expand Down Expand Up @@ -832,3 +832,138 @@ describe("testPointOnLine", () => {
});
});
});

describe("mergetSnappingResult", () => {
test("should merge two snapping results in each axis", () => {
const a: SnappingResult = {
diff: { x: 1, y: 20 },
targets: [
{
id: "a1",
line: [
{ x: 1, y: 0 },
{ x: 1, y: 10 },
],
},
{
id: "a2",
line: [
{ x: 0, y: 20 },
{ x: 10, y: 20 },
],
},
],
intervalTargets: [
{
beforeId: "ab1",
afterId: "aa1",
lines: [
[
{ x: 1, y: 0 },
{ x: 5, y: 0 },
],
],
direction: "h",
},
{
beforeId: "ab2",
afterId: "aa2",
lines: [
[
{ x: 0, y: 20 },
{ x: 0, y: 25 },
],
],
direction: "v",
},
],
};
const b: SnappingResult = {
diff: { x: -20, y: -2 },
targets: [
{
id: "b1",
line: [
{ x: -20, y: 0 },
{ x: -20, y: 10 },
],
},
{
id: "b2",
line: [
{ x: 0, y: -2 },
{ x: 10, y: -2 },
],
},
],
intervalTargets: [
{
beforeId: "bb1",
afterId: "ba1",
lines: [
[
{ x: -20, y: 0 },
{ x: -5, y: 0 },
],
],
direction: "h",
},
{
beforeId: "bb2",
afterId: "ba2",
lines: [
[
{ x: 0, y: -2 },
{ x: 0, y: 5 },
],
],
direction: "v",
},
],
};

expect(mergetSnappingResult(a, b)).toEqual({
diff: { x: 1, y: -2 },
targets: [
{
id: "a1",
line: [
{ x: 1, y: 0 },
{ x: 1, y: 10 },
],
},
{
id: "b2",
line: [
{ x: 0, y: -2 },
{ x: 10, y: -2 },
],
},
],
intervalTargets: [
{
beforeId: "ab1",
afterId: "aa1",
lines: [
[
{ x: 1, y: 0 },
{ x: 5, y: 0 },
],
],
direction: "h",
},
{
beforeId: "bb2",
afterId: "ba2",
lines: [
[
{ x: 0, y: -2 },
{ x: 0, y: 5 },
],
],
direction: "v",
},
],
});
});
});
62 changes: 61 additions & 1 deletion src/composables/shapeSnapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,23 @@ export function newShapeSnapping(option: Option) {
return { targets, intervalTargets, diff };
}

/**
* Proc "test" with two rectangles and merge their results.
*/
function testWithSubRect(
rectMain: IRectangle,
rectSub?: IRectangle,
option?: TestOption,
): SnappingResult | undefined {
const resultMain = test(rectMain, option);
if (!rectSub) return resultMain;

const resultSub = test(rectSub, option);
if (resultMain && resultSub) return mergetSnappingResult(resultMain, resultSub);

return resultMain ?? resultSub;
}

function testPoint(p: IVec2): SnappingResult | undefined {
let xClosest: [string, SnappingTmpResult] | undefined;
let yClosest: [string, SnappingTmpResult] | undefined;
Expand Down Expand Up @@ -316,7 +333,7 @@ export function newShapeSnapping(option: Option) {
});
}

return { test, testPoint, testPointOnLine, snapThreshold };
return { test, testWithSubRect, testPoint, testPointOnLine, snapThreshold };
}
export type ShapeSnapping = ReturnType<typeof newShapeSnapping>;

Expand Down Expand Up @@ -883,3 +900,46 @@ export function filterSnappingTargetsBySecondGuideline(
),
};
}

export function mergetSnappingResult(a: SnappingResult, b: SnappingResult): SnappingResult {
// Filter guildelines for x-axis.
const infoAX = getSecondGuidelineCandidateInfo(a, { x: 1, y: 0 });
const infoBX = getSecondGuidelineCandidateInfo(b, { x: 1, y: 0 });
let diffX = a.diff.x;
let infoX = infoAX;
if (infoAX.targets.length > 0 || infoAX.intervalTargets.length > 0) {
if (infoBX.targets.length > 0 || infoBX.intervalTargets.length > 0) {
// When both results have guidelines, pick one having smaller diff.
if (Math.abs(a.diff.x) > Math.abs(b.diff.x)) {
diffX = b.diff.x;
infoX = infoBX;
}
}
} else {
diffX = b.diff.x;
infoX = infoBX;
}

// Parallel to x-axis.
const infoAY = getSecondGuidelineCandidateInfo(a, { x: 0, y: 1 });
const infoBY = getSecondGuidelineCandidateInfo(b, { x: 0, y: 1 });
let diffY = a.diff.y;
let infoY = infoAY;
if (infoAY.targets.length > 0 || infoAY.intervalTargets.length > 0) {
if (infoBY.targets.length > 0 || infoBY.intervalTargets.length > 0) {
if (Math.abs(a.diff.y) > Math.abs(b.diff.y)) {
diffY = b.diff.y;
infoY = infoBY;
}
}
} else {
diffY = b.diff.y;
infoY = infoBY;
}

return {
diff: { x: diffX, y: diffY },
targets: [...infoX.targets, ...infoY.targets],
intervalTargets: [...infoX.intervalTargets, ...infoY.intervalTargets],
};
}
15 changes: 14 additions & 1 deletion src/composables/states/appCanvas/movingShapeState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ interface Option {
export function newMovingShapeState(option?: Option): AppCanvasState {
let shapeSnapping: ShapeSnapping;
let movingRect: IRectangle;
let movingRectSub: IRectangle | undefined;
let boundingBox: BoundingBox;
let snappingResult: SnappingResult | undefined;
let affine = IDENTITY_AFFINE;
Expand Down Expand Up @@ -71,6 +72,11 @@ export function newMovingShapeState(option?: Option): AppCanvasState {
});
movingRect = geometry.getWrapperRect(targetIds.map((id) => shapeComposite.getWrapperRect(shapeMap[id])));

// When multiple shapes are selected, use the bounds of the index shape as snapping source.
if (targetIds.length > 1 && indexShapeId) {
movingRectSub = shapeComposite.getWrapperRect(shapeMap[indexShapeId]);
}

if (option?.boundingBox) {
boundingBox = option.boundingBox;
} else {
Expand Down Expand Up @@ -111,7 +117,14 @@ export function newMovingShapeState(option?: Option): AppCanvasState {
}

const d = sub(event.data.current, event.data.start);
snappingResult = event.data.ctrl ? undefined : shapeSnapping.test(moveRect(movingRect, d));

snappingResult = event.data.ctrl
? undefined
: (snappingResult = shapeSnapping.testWithSubRect(
moveRect(movingRect, d),
movingRectSub ? moveRect(movingRectSub, d) : undefined,
));

const translate = snappingResult ? add(d, snappingResult.diff) : d;
affine = [1, 0, 0, 1, translate.x, translate.y];

Expand Down

0 comments on commit a7c9776

Please sign in to comment.