Skip to content

Commit

Permalink
feat: Add a shortcut to adjust line label's alignments withough movin…
Browse files Browse the repository at this point in the history
…g the atatching point
  • Loading branch information
miyanokomiya committed Nov 26, 2023
1 parent 1da44aa commit 1cc8c90
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 14 deletions.
49 changes: 48 additions & 1 deletion src/composables/lineLabelHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { describe, test, expect } from "vitest";
import { createShape, getCommonStruct } from "../shapes";
import { LineShape } from "../shapes/line";
import { TextShape } from "../shapes/text";
import { newLineLabelHandler } from "./lineLabelHandler";
import { getPatchByUpdateLabelAlign, newLineLabelHandler } from "./lineLabelHandler";
import { createStrokeStyle } from "../utils/strokeStyle";
import { newShapeComposite } from "./shapeComposite";

Expand Down Expand Up @@ -80,3 +80,50 @@ describe("newLineLabelHandler", () => {
});
});
});

describe("getPatchByUpdateLabelAlign", () => {
const line0 = createShape<LineShape>(getCommonStruct, "line", {
id: "line0",
p: { x: 0, y: 0 },
q: { x: 100, y: 100 },
});
const label0 = createShape<TextShape>(getCommonStruct, "text", {
id: "label0",
parentId: "line0",
lineAttached: 0.5,
width: 10,
height: 10,
});

test("should return patch object to update the label's aligns and position", () => {
expect(getPatchByUpdateLabelAlign(line0, label0, { x: 48, y: 52 })).toEqual({
hAlign: "center",
vAlign: "center",
p: { x: 45, y: 45 },
});

const ret1 = getPatchByUpdateLabelAlign(line0, label0, { x: 20, y: 20 });
expect(ret1.hAlign).toBe("right");
expect(ret1.vAlign).toBe("bottom");
expect(ret1.p?.x).toBeCloseTo(35.05, 3);
expect(ret1.p?.y).toBeCloseTo(35.05, 3);

const ret2 = getPatchByUpdateLabelAlign(line0, label0, { x: 80, y: 80 });
expect(ret2.hAlign).toBe("left");
expect(ret2.vAlign).toBe("top");
expect(ret2.p?.x).toBeCloseTo(54.95, 3);
expect(ret2.p?.y).toBeCloseTo(54.95, 3);
});

test("should drop attributes that are same to the originals", () => {
expect(
getPatchByUpdateLabelAlign(line0, { ...label0, hAlign: "center", vAlign: "center" }, { x: 48, y: 52 }),
).toEqual({
p: { x: 45, y: 45 },
});
expect(getPatchByUpdateLabelAlign(line0, { ...label0, p: { x: 45, y: 45 } }, { x: 48, y: 52 })).toEqual({
hAlign: "center",
vAlign: "center",
});
});
});
23 changes: 23 additions & 0 deletions src/composables/lineLabelHandler.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { IVec2 } from "okageo";
import { EntityPatchInfo, Shape } from "../models";
import { LineShape, getLinePath, getRelativePointOn, isCurveLine, isLineShape } from "../shapes/line";
import { TextShape, isLineLabelShape, patchPosition } from "../shapes/text";
Expand Down Expand Up @@ -78,6 +79,28 @@ function getLabelMargin(line: LineShape): number {
return (line.stroke.width ?? 1) / 2 + 6;
}

/**
* Returns patch object to update the lable's aligns and position based on the parent line and the given point.
*/
export function getPatchByUpdateLabelAlign(parentLine: LineShape, label: TextShape, targetP: IVec2, scale = 1) {
const origin = getRelativePointOn(parentLine, label.lineAttached ?? 0.5);
const size = 20 * scale;
let patched: Partial<TextShape> = {
hAlign: targetP.x < origin.x - size ? "right" : origin.x + size < targetP.x ? "left" : "center",
vAlign: targetP.y < origin.y - size ? "bottom" : origin.y + size < targetP.y ? "top" : "center",
};
patched = { ...patched, ...patchPosition({ ...label, ...patched }, origin, getLabelMargin(parentLine)) };

if (patched.hAlign === label.hAlign) {
delete patched.hAlign;
}
if (patched.vAlign === label.vAlign) {
delete patched.vAlign;
}

return patched;
}

export function getLineLabelPatch(srcComposite: ShapeComposite, patchInfo: EntityPatchInfo<Shape>) {
if (!patchInfo.update) return {};

Expand Down
3 changes: 3 additions & 0 deletions src/composables/states/appCanvas/commandExams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ export const COMMAND_EXAM_SRC = {
DISABLE_SNAP: { command: getCtrlOrMetaStr(), title: "Disable snapping" },
DISABLE_LINE_VERTEX_SNAP: { command: getCtrlOrMetaStr(), title: "Disable snapping" },

LABEL_ALIGN: { command: "Shift", title: "Adjust label align" },
LABEL_ALIGN_ACTIVATE: { command: "Shift + Drag", title: "Adjust label align" },

RESIZE_PROPORTIONALLY: { command: "Shift", title: "Proportionally" },
RESIZE_AT_CENTER: { command: getAltOrOptionStr(), title: "Based on center" },

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import { renderParentLineRelation } from "../../../lineLabelHandler";
import { newRotatingLineLabelState } from "./rotatingLineLabelState";
import { CONTEXT_MENU_ITEM_SRC, handleContextItemEvent } from "../contextMenuItems";
import { findBetterShapeAt } from "../../../shapeComposite";
import { COMMAND_EXAM_SRC } from "../commandExams";

interface Option {
boundingBox?: BoundingBox;
Expand All @@ -37,7 +38,7 @@ export function newLineLabelSelectedState(option?: Option): AppCanvasState {
return {
getLabel: () => "LineLabelSelected",
onStart: (ctx) => {
ctx.setCommandExams(getCommonCommandExams(ctx));
ctx.setCommandExams([COMMAND_EXAM_SRC.LABEL_ALIGN_ACTIVATE, ...getCommonCommandExams(ctx)]);

const shapeComposite = ctx.getShapeComposite();
const shapeMap = shapeComposite.shapeMap;
Expand Down
52 changes: 40 additions & 12 deletions src/composables/states/appCanvas/lines/movingLineLabelState.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
import type { AppCanvasState } from "../core";
import { AffineMatrix, IDENTITY_AFFINE, sub } from "okageo";
import { mapReduce, patchPipe } from "../../../../utils/commons";
import { LineLabelHandler, newLineLabelHandler, renderParentLineRelation } from "../../../lineLabelHandler";
import {
LineLabelHandler,
getPatchByUpdateLabelAlign,
newLineLabelHandler,
renderParentLineRelation,
} from "../../../lineLabelHandler";
import { resizeShape } from "../../../../shapes";
import { newSelectionHubState } from "../selectionHubState";
import { BoundingBox } from "../../../boundingBox";
import { LineShape } from "../../../../shapes/line";
import { TextShape } from "../../../../shapes/text";
import { Shape } from "../../../../models";
import { COMMAND_EXAM_SRC } from "../commandExams";

interface Option {
boundingBox: BoundingBox;
Expand All @@ -24,6 +31,7 @@ export function newMovingLineLabelState(option: Option): AppCanvasState {
onStart: (ctx) => {
ctx.startDragging();
ctx.setCursor("move");
ctx.setCommandExams([COMMAND_EXAM_SRC.LABEL_ALIGN]);

const id = ctx.getLastSelectedShapeId();
const shapeMap = ctx.getShapeComposite().shapeMap;
Expand All @@ -39,23 +47,43 @@ export function newMovingLineLabelState(option: Option): AppCanvasState {
ctx.stopDragging();
ctx.setTmpShapeMap({});
ctx.setCursor();
ctx.setCommandExams();
},
handleEvent: (ctx, event) => {
switch (event.type) {
case "pointermove": {
const d = sub(event.data.current, event.data.start);
const affineSrc: AffineMatrix = [1, 0, 0, 1, d.x, d.y];
const patched = patchPipe(
[
(src) => mapReduce(src, (s) => resizeShape(ctx.getShapeStruct, s, affineSrc)),
(_src, patch) => lineLabelHandler.onModified(patch),
],
{ [labelShape.id]: labelShape },
);
ctx.setTmpShapeMap(patched.patch);
let patch: { [id: string]: Partial<Shape> };

if (event.data.shift) {
// Keep the latest label position.
const lineAttached =
(ctx.getTmpShapeMap()[labelShape.id] as TextShape)?.lineAttached ?? labelShape.lineAttached;
patch = {
[labelShape.id]: {
lineAttached,
...getPatchByUpdateLabelAlign(
parentLineShape,
{ ...labelShape, lineAttached },
event.data.current,
ctx.getScale(),
),
},
};
} else {
const d = sub(event.data.current, event.data.start);
const affineSrc: AffineMatrix = [1, 0, 0, 1, d.x, d.y];
patch = patchPipe(
[
(src) => mapReduce(src, (s) => resizeShape(ctx.getShapeStruct, s, affineSrc)),
(_src, patch) => lineLabelHandler.onModified(patch),
],
{ [labelShape.id]: labelShape },
).patch;
}

ctx.setTmpShapeMap(patch);
// Save final transition as current affine
const updated = patched.patch[labelShape.id];
const updated = patch[labelShape.id];
affine = updated.p
? [1, 0, 0, 1, updated.p.x - labelShape.p.x, updated.p.y - labelShape.p.y]
: IDENTITY_AFFINE;
Expand Down

0 comments on commit 1cc8c90

Please sign in to comment.