Skip to content

Commit

Permalink
feat: Show the rotation anchor for selected line
Browse files Browse the repository at this point in the history
- The rotation value is reset every time because lines don't have rotation.
  • Loading branch information
miyanokomiya committed Oct 12, 2024
1 parent 7bb1c27 commit 02e3a51
Show file tree
Hide file tree
Showing 3 changed files with 71 additions and 25 deletions.
5 changes: 5 additions & 0 deletions src/composables/lineBounding.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ describe("newLineBounding", () => {
index: 0,
});

expect(target.hitTest({ x: 0, y: -30 })).toEqual({
type: "rotate-anchor",
index: 0,
});

expect(target.hitTest({ x: -1, y: 0 })).toEqual({
type: "vertex",
index: 0,
Expand Down
79 changes: 54 additions & 25 deletions src/composables/lineBounding.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { IVec2, add, getCenter, getRadian, isSame, multi, rotate } from "okageo";
import { IVec2, add, getCenter, getOuterRectangle, getRadian, isSame, multi, rotate } from "okageo";
import { BezierCurveControl, StyleScheme } from "../models";
import { LineShape, getConnections, getEdges, getLinePath, getRadianP, getRadianQ, isCurveLine } from "../shapes/line";
import { newCircleHitTest } from "./shapeHitTest";
Expand All @@ -12,20 +12,22 @@ import {
renderOutlinedCircle,
renderOutlinedDonutArc,
renderPlusIcon,
renderRotationArrow,
} from "../utils/renderer";
import { getSegmentVicinityFrom, getSegmentVicinityTo } from "../utils/path";
import { canAddBezierControls, getModifiableBezierControls } from "../shapes/utils/curveLine";

const VERTEX_R = 7;
const ADD_VERTEX_ANCHOR_RATE = 1;
const MOVE_ANCHOR_RATE = 1.4;
const BOUNDS_ANCHOR_SIZE = VERTEX_R * 1.4;
export const BEZIER_ANCHOR_SIZE = 6;
const BEZIER_ANCHOR_VICINITY_SIZE = 14;
const BEZIER_ANCHOR_VICINITY_INNER_RATE = 0.4;
const BEZIER_DONUT_RAD = Math.PI / 3;

type LineHitType =
| "move-anchor"
| "rotate-anchor"
| "vertex"
| "segment"
| "new-vertex-anchor"
Expand Down Expand Up @@ -78,6 +80,7 @@ export function newLineBounding(option: Option) {
return lerpFn(0.5);
});
const bezierAnchors = getModifiableBezierControls(lineShape);
const vertexWrapperRect = getOuterRectangle([vertices]);

const elbow = isElbow(lineShape);
const availableVertexIndex = elbow ? new Set([0, vertices.length - 1]) : new Set(vertices.map((_, i) => i));
Expand Down Expand Up @@ -106,6 +109,11 @@ export function newLineBounding(option: Option) {
return add(vertices[0], multi(v, scale));
}

function getRotateAnchor(scale: number): IVec2 {
const v = rotate({ x: 0, y: 30 }, getRadianP(lineShape));
return add(vertices[0], multi(v, scale));
}

function getAddAnchorP(scale: number): IVec2 {
const v = rotate({ x: 20, y: 0 }, getRadianP(lineShape));
return add(lineShape.p, multi(v, scale));
Expand Down Expand Up @@ -169,15 +177,24 @@ export function newLineBounding(option: Option) {
const vertexSize = VERTEX_R * scale;
const bezierSize = BEZIER_ANCHOR_SIZE * scale;
const bezierVicinitySize = BEZIER_ANCHOR_VICINITY_SIZE * scale;
const boundsAnchorSize = BOUNDS_ANCHOR_SIZE * scale;

{
const moveAnchor = getMoveAnchor(scale);
const testFn = newCircleHitTest(moveAnchor, vertexSize * MOVE_ANCHOR_RATE);
const testFn = newCircleHitTest(moveAnchor, boundsAnchorSize);
if (testFn.test(p)) {
return { type: "move-anchor", index: 0 };
}
}

{
const moveAnchor = getRotateAnchor(scale);
const testFn = newCircleHitTest(moveAnchor, boundsAnchorSize);
if (testFn.test(p)) {
return { type: "rotate-anchor", index: 0 };
}
}

{
const optimizeAnchorP = getOptimizeAnchorP(scale);
if (optimizeAnchorP) {
Expand Down Expand Up @@ -286,6 +303,7 @@ export function newLineBounding(option: Option) {
const vertexSize = VERTEX_R * scale;
const bezierSize = BEZIER_ANCHOR_SIZE * scale;
const bezierVicinitySize = BEZIER_ANCHOR_VICINITY_SIZE * scale;
const boundsAnchorSize = BOUNDS_ANCHOR_SIZE * scale;
const style = option.styleScheme;

const addAnchorBeziers = getAddAnchorBeziers(scale);
Expand Down Expand Up @@ -357,10 +375,20 @@ export function newLineBounding(option: Option) {
{
applyFillStyle(ctx, { color: style.selectionPrimary });
ctx.beginPath();
ctx.ellipse(moveAnchor.x, moveAnchor.y, vertexSize * MOVE_ANCHOR_RATE, vertexSize * MOVE_ANCHOR_RATE, 0, 0, TAU);
ctx.arc(moveAnchor.x, moveAnchor.y, boundsAnchorSize, 0, TAU);
ctx.fill();
ctx.fillStyle = "#fff";
renderMoveIcon(ctx, moveAnchor, boundsAnchorSize);
}

const rotateAnchor = getRotateAnchor(scale);
{
applyFillStyle(ctx, { color: style.selectionPrimary });
ctx.beginPath();
ctx.arc(rotateAnchor.x, rotateAnchor.y, boundsAnchorSize, 0, TAU);
ctx.fill();
ctx.fillStyle = "#fff";
renderMoveIcon(ctx, moveAnchor, vertexSize * MOVE_ANCHOR_RATE);
renderRotationArrow(ctx, rotateAnchor, 0, boundsAnchorSize);
}

const optimizeAnchorP = getOptimizeAnchorP(scale);
Expand Down Expand Up @@ -389,31 +417,32 @@ export function newLineBounding(option: Option) {
applyStrokeStyle(ctx, { color: style.selectionSecondaly, width: style.selectionLineWidth * scale });
applyFillStyle(ctx, { color: style.selectionSecondaly });
ctx.beginPath();
ctx.arc(moveAnchor.x, moveAnchor.y, vertexSize * MOVE_ANCHOR_RATE, 0, TAU);
ctx.arc(moveAnchor.x, moveAnchor.y, boundsAnchorSize, 0, TAU);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#fff";
renderMoveIcon(ctx, moveAnchor, vertexSize * MOVE_ANCHOR_RATE);

applyStrokeStyle(ctx, { color: style.selectionPrimary, width: 3 * scale });
edges.forEach((edge, i) => {
ctx.beginPath();
if (curves) {
applyCurvePath(ctx, edge, [curves[i]]);
} else {
applyPath(ctx, edge);
}
ctx.stroke();
});
renderMoveIcon(ctx, moveAnchor, boundsAnchorSize);

vertices.forEach((p, i) => {
if (!availableVertexIndex.has(i)) return;
applyStrokeStyle(ctx, { color: style.selectionSecondaly, width: 3 * scale });
ctx.beginPath();
ctx.rect(vertexWrapperRect.x, vertexWrapperRect.y, vertexWrapperRect.width, vertexWrapperRect.height);
ctx.stroke();
break;
}
case "rotate-anchor": {
applyStrokeStyle(ctx, { color: style.selectionSecondaly, width: style.selectionLineWidth * scale });
applyFillStyle(ctx, { color: style.selectionSecondaly });
ctx.beginPath();
ctx.arc(rotateAnchor.x, rotateAnchor.y, boundsAnchorSize, 0, TAU);
ctx.fill();
ctx.stroke();
ctx.fillStyle = "#fff";
renderRotationArrow(ctx, rotateAnchor, 0, boundsAnchorSize);

ctx.beginPath();
ctx.ellipse(p.x, p.y, vertexSize, vertexSize, 0, 0, TAU);
ctx.fill();
ctx.stroke();
});
applyStrokeStyle(ctx, { color: style.selectionSecondaly, width: 3 * scale });
ctx.beginPath();
ctx.rect(vertexWrapperRect.x, vertexWrapperRect.y, vertexWrapperRect.width, vertexWrapperRect.height);
ctx.stroke();
break;
}
case "vertex": {
Expand Down
12 changes: 12 additions & 0 deletions src/composables/states/appCanvas/lines/lineSelectedState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ import { newMovingElbowSegmentState } from "./movingElbowSegmentState";
import { newElbowLineHandler } from "../../../elbowLineHandler";
import { newMovingLineBezierState } from "./movingLineBezierState";
import { isObjectEmpty } from "../../../../utils/commons";
import { newRotatingState } from "../rotatingState";
import { newBoundingBox } from "../../../boundingBox";

type VertexMetaForContextMenu = {
index: number;
Expand Down Expand Up @@ -87,6 +89,16 @@ export const newLineSelectedState = defineIntransientState(() => {
}
case "move-anchor":
return newMovingHubState;
case "rotate-anchor": {
const shapeComposite = ctx.getShapeComposite();
const rectPath = shapeComposite.getLocalRectPolygon(lineShape);
return () =>
newRotatingState({
boundingBox: newBoundingBox({
path: rectPath,
}),
});
}
case "vertex":
if (event.data.options.shift) {
const patch = deleteVertex(lineShape, hitResult.index);
Expand Down

0 comments on commit 02e3a51

Please sign in to comment.