Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(): Convert align guidelines to TS. Add build system for extensions #10043

Merged
merged 26 commits into from
Aug 11, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## [next]

- feat(): Add v6 aligning guidelines. [#10033](https://github.com/fabricjs/fabric.js/discussions/10033)
- feat(): Add has method to classRegistry to allow to check if a class exists. (fixes #10001)

## [6.1.0]
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,16 @@ import { fabric } from 'fabric';
fill: 'red',
});
canvas.add(rect);

// load aligning_guidelines
fabric.initAligningGuidelines(canvas, {
/** Aligning line color */
color: 'rgb(255,0,0,0.9)',
/** Aligning line dimensions */
width: 1,
/** At what distance from the shape does alignment begin? */
margin: 4,
});
</script>
```

Expand Down
2 changes: 2 additions & 0 deletions fabric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,3 +178,5 @@ export { Control } from './src/controls/Control';
export * as controlsUtils from './src/controls';

export * from './src/filters';

export { initAligningGuidelines } from './lib/aligning_guidelines/index';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is the issue i want to solve. I don't want to package those in this way.
I ll need to work out a way for rollup to produce a different bundle that has fabric has an external dep and that is reacheable from fabric/plugin or something like that

27 changes: 27 additions & 0 deletions lib/aligning_guidelines/@types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export type VerticalLine = {
x: number;
y1: number;
y2: number;
};

export type HorizontalLine = {
y: number;
x1: number;
x2: number;
};

export type VerticalLineProps = {
x: number;
objectY: number;
objectHeight: number;
activeObjectY: number;
activeObjectHeight: number;
};

export type HorizontalLineProps = {
y: number;
objectX: number;
objectWidth: number;
activeObjectX: number;
activeObjectWidth: number;
};
9 changes: 9 additions & 0 deletions lib/aligning_guidelines/constant.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const AligningLineConfig = {
offset: 0,
/** At what distance from the shape does alignment begin? */
margin: 4,
/** Aligning line dimensions */
width: 1,
/** Aligning line color */
color: "rgb(255,0,0,0.9)"
}
133 changes: 133 additions & 0 deletions lib/aligning_guidelines/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type { BasicTransformEvent, Canvas, FabricObject, TBBox, TPointerEvent } from "../../fabric";
import { Point } from "../../src/Point";
import {
collectHorizontalPoint,
collectLine,
collectVerticalPoint,
drawHorizontalLine,
drawPointList,
drawVerticalLine,
getObjectsByTarget,
} from "./util";
import { makeBoundingBoxFromPoints } from "../../src/util/misc/boundingBoxFromPoints";
import { HorizontalLine, VerticalLine } from "./@types";
import { AligningLineConfig } from "./constant";

type TransformEvent = BasicTransformEvent<TPointerEvent> & { target: FabricObject };
type AlignOptions = Partial<typeof AligningLineConfig>;
export function initAligningGuidelines(canvas: Canvas, options: AlignOptions = {}) {
Object.assign(AligningLineConfig, options);

const horizontalLines = new Set<string>();
const verticalLines = new Set<string>();
let onlyDrawPoint = false;
const cacheMap = new Map<string, [TBBox, Point[]]>();

const getCaCheMapValue = (object: FabricObject) => {
const cacheKey = [object.calcTransformMatrix().toString(), object.width, object.height].join();
const cacheValue = cacheMap.get(cacheKey);
if (cacheValue) return cacheValue;
const coords = object.getCoords();
const rect = makeBoundingBoxFromPoints(coords);
const value: [TBBox, Point[]] = [rect, coords];
cacheMap.set(cacheKey, value);
return value;
};

function moving(e: TransformEvent) {
const activeObject = e.target;
activeObject.setCoords();
onlyDrawPoint = false;
verticalLines.clear();
horizontalLines.clear();

const objects = getObjectsByTarget(activeObject);
const activeObjectRect = activeObject.getBoundingRect();

for (const object of objects) {
const objectRect = getCaCheMapValue(object)[0];
const { vLines, hLines } = collectLine({ activeObject, activeObjectRect, objectRect });
vLines.forEach((o) => {
verticalLines.add(JSON.stringify(o));
});
hLines.forEach((o) => {
horizontalLines.add(JSON.stringify(o));
});
}
}

function scalingOrResizing(e: TransformEvent) {
// br bl tr tl mb ml mt mr
const activeObject = e.target;
activeObject.setCoords();
const isScale = String(e.transform.action).startsWith("scale");
verticalLines.clear();
horizontalLines.clear();

const objects = getObjectsByTarget(activeObject);
let corner = e.transform.corner;
if (activeObject.flipX) corner = corner.replace("l", "r").replace("r", "l");
if (activeObject.flipY) corner = corner.replace("t", "b").replace("b", "t");
let index = ["tl", "tr", "br", "bl", "mt", "mr", "mb", "ml"].indexOf(corner);
if (index == -1) return;
onlyDrawPoint = index > 3;
if (onlyDrawPoint) {
const angle = activeObject.getTotalAngle();
if (angle % 90 != 0) return;
index -= 4;
}
let point = activeObject.getCoords()[index];
for (const object of objects) {
const [rect, coords] = getCaCheMapValue(object);
const center = new Point(rect.left + rect.width / 2, rect.top + rect.height / 2);
const list = [...coords, center];
const props = { activeObject, point, list, isScale, index };
const vLines = collectVerticalPoint(props);
const hLines = collectHorizontalPoint(props);
vLines.forEach((o) => {
verticalLines.add(JSON.stringify(o));
});
hLines.forEach((o) => {
horizontalLines.add(JSON.stringify(o));
});
if (vLines.length || hLines.length) point = activeObject.getCoords()[index];
}
}

function beforeRender() {
canvas.clearContext(canvas.contextTop);
}
function afterRender() {
if (onlyDrawPoint) {
const list: Array<VerticalLine | HorizontalLine> = [];
for (const v of verticalLines) list.push(JSON.parse(v));
for (const h of horizontalLines) list.push(JSON.parse(h));
drawPointList(canvas, list);
} else {
for (const v of verticalLines) drawVerticalLine(canvas, JSON.parse(v));
for (const h of horizontalLines) drawHorizontalLine(canvas, JSON.parse(h));
}
}
function mouseUp() {
verticalLines.clear();
horizontalLines.clear();
cacheMap.clear();
canvas.requestRenderAll();
}

canvas.on("object:resizing", scalingOrResizing);
canvas.on("object:scaling", scalingOrResizing);
canvas.on("object:moving", moving);
canvas.on("before:render", beforeRender);
canvas.on("after:render", afterRender);
canvas.on("mouse:up", mouseUp);

return () => {
canvas.off("object:resizing", scalingOrResizing);
canvas.off("object:scaling", scalingOrResizing);
canvas.off("object:moving", moving);
canvas.off("before:render", beforeRender);
canvas.off("after:render", afterRender);
canvas.off("mouse:up", mouseUp);
};
}
12 changes: 12 additions & 0 deletions lib/aligning_guidelines/util/basic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type { FabricObject, Point } from "../../../fabric";

export function getDistance(a: number, b: number) {
return Math.abs(a - b);
}

export function setPositionDir(target: FabricObject, pos: Point, dir: "x" | "y") {
const center = target.translateToCenterPoint(pos, "center", "center");
const position = target.translateToOriginPoint(center, target.originX, target.originY);
if (dir == "x") target.setX(position.x);
else target.setY(position.y);
}
150 changes: 150 additions & 0 deletions lib/aligning_guidelines/util/collect-line.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import type { FabricObject, TBBox } from "../../../fabric";
import { Point } from "../../../src/Point";
import { HorizontalLine, VerticalLine } from "../@types";
import { AligningLineConfig } from "../constant";
import { getDistance, setPositionDir } from "./basic";

type CollectLineProps = {
activeObject: FabricObject;
activeObjectRect: TBBox;
objectRect: TBBox;
};

export function collectLine(props: CollectLineProps) {
const aligningLineMargin = AligningLineConfig.margin;
const { activeObject, activeObjectRect, objectRect } = props;
const list = makeLineByRect(objectRect);
const aList = makeLineByRect(activeObjectRect);
const margin = aligningLineMargin / (activeObject.canvas?.getZoom() ?? 1);
const opts = { target: activeObject, list, aList, margin };
const vLines = collectVerticalLine(opts);
const hLines = collectHorizontalLine(opts);

return { vLines, hLines };
}

type CollectItemLineProps = {
target: FabricObject;
list: LineProps[];
aList: LineProps[];
margin: number;
};
function collectVerticalLine(props: CollectItemLineProps) {
const { target, list, aList, margin } = props;

const arr = aList.map((x) => getDistanceLine(x, list, "x"));
const min = Math.min(...arr.map((x) => x.dis));
if (min > margin) return [];
const lines: VerticalLine[] = [];
const width = aList[0].x2 - aList[0].x;
const height = aList[0].y2 - aList[0].y;
let b = false;
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
if (min == item.dis) {
const line = list[item.index];
const aLine = aList[item.index];
let x = line.x;
const y = aLine.y;

const y1 = Math.min(line.y, line.y2, y, aLine.y2);
const y2 = Math.max(line.y, line.y2, y, aLine.y2);
// 参考线可画多条
lines.push({ x, y1, y2 });
if (b) continue;
b = true;
// 对齐只进行一次
setPos({ target, x, y, centerX: i - 1, centerY: item.index - 1, width, height, dir: "x" });
const dis = min * item.dir;
aList.forEach((x) => (x.x -= dis));
}
}
return lines;
}

function collectHorizontalLine(props: CollectItemLineProps) {
const { target, list, aList, margin } = props;

const arr = aList.map((x) => getDistanceLine(x, list, "y"));
const min = Math.min(...arr.map((x) => x.dis));
if (min > margin) return [];
const lines: HorizontalLine[] = [];
const width = aList[0].x2 - aList[0].x;
const height = aList[0].y2 - aList[0].y;
let b = false;
for (let i = 0; i < arr.length; i++) {
const item = arr[i];
if (min == item.dis) {
const line = list[item.index];
const aLine = aList[item.index];
let y = line.y;
const x = aLine.x;

const x1 = Math.min(line.x, line.x2, x, aLine.x2);
const x2 = Math.max(line.x, line.x2, x, aLine.x2);
// 参考线可画多条
lines.push({ y, x1, x2 });
if (b) continue;
b = true;
// 对齐只进行一次
setPos({ target, x, y, centerX: item.index - 1, centerY: i - 1, width, height, dir: "y" });
const dis = min * item.dir;
aList.forEach((x) => (x.y -= dis));
}
}
return lines;
}

type LineProps = {
x: number;
y: number;
x2: number;
y2: number;
};
function getDistanceLine(target: LineProps, list: LineProps[], type: "x" | "y") {
let dis = Infinity;
let index = -1;
/** 1正值 -1负值 */
let dir = 1;
for (let i = 0; i < list.length; i++) {
const v = getDistance(target[type], list[i][type]);
if (dis > v) {
index = i;
dis = v;
dir = target[type] > list[i][type] ? 1 : -1;
}
}
return { dis, index, dir };
}

function makeLineByRect(rect: TBBox) {
const { left, top, width, height } = rect;
const a = { x: left, y: top, x2: left + width, y2: top + height };
const x = left + width / 2;
const y = top + height / 2;
const b = { x, y, x2: x, y2: y };
const c = { x: left + width, x2: left, y: top + height, y2: top };

return [a, b, c];
}

type SnapToPixelProps = {
target: FabricObject;
x: number;
y: number;
/** -1 0 1 */
centerX: number;
/** -1 0 1 */
centerY: number;
width: number;
height: number;
dir: "x" | "y";
};
function setPos(props: SnapToPixelProps) {
const { target, centerX, centerY, width, height, dir } = props;
let { x, y } = props;
x -= (centerX * width) / 2;
y -= (centerY * height) / 2;
setPositionDir(target, new Point(x, y), dir);
target.setCoords();
}
Loading