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

update(AligningGuidelines): Fix some bugs and add custom features. #10120

Open
wants to merge 15 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [next]

- fix(AligningGuidelines): Too many shapes will result in too many reference lines [#10120] (https://github.com/fabricjs/fabric.js/pull/10120)
- fix(AligningGuidelines): Align guidless changes aspect ratio on snapping when scaling [#10114](https://github.com/fabricjs/fabric.js/issues/10114)
- fix(): for object caching over invalidating the cache [#10294](https://github.com/fabricjs/fabric.js/pull/10294)

## [6.5.1]
Expand Down
75 changes: 74 additions & 1 deletion extensions/aligning_guidelines/README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,11 @@ const config = {
/** Aligning line dimensions */
width: 1,
/** Aligning line color */
color: 'rgb(255,0,0,0.9)',
color: 'rgba(255,0,0,0.9)',
/** Close Vertical line, default false. */
closeVLine: false,
/** Close horizontal line, default false. */
closeHLine: false,
};

const deactivate = initAligningGuidelines(myCanvas, options);
Expand All @@ -20,3 +24,72 @@ const deactivate = initAligningGuidelines(myCanvas, options);

deactivate();
```

### custom function

```ts
import { initAligningGuidelines } from 'fabric/extensions';
import { FabricObject } from 'fabric';

// You can customize the return graphic, and the example will only compare it with sibling elements
initAligningGuidelines(myCanvas, {
getObjectsByTarget: function (target) {
Copy link

Choose a reason for hiding this comment

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

I tried this PR and tried using this function to filter out objects that have properties isGridLine on them
like if (!object.isGridLine) { set.add(object) }
but then no guidelines happened at all
was I using it wrong maybe?

Copy link
Contributor Author

@zhe-he zhe-he Nov 7, 2024

Choose a reason for hiding this comment

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

@Smrtnyk
Can you provide a simple demo to reproduce your issue? This will help me locate the problem. Regarding this function, if customized, it means that when moving the target element, it only processes the reference line for the graphical representation of the return value
The input parameter of this function is the current shape being operated on, not a loop representing all shapes.

Copy link

Choose a reason for hiding this comment

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

this is what I tried

 initAligningGuidelines(this.canvas, {
         getObjectsByTarget: () => {
             const set = new Set<FabricObject>();
             for (const obj of this.canvas.getObjects()) {
                 if (obj.isGridLine) {
                     continue;
                 }
                 set.add(obj);
             }
             return set;
         },
     });

I have a grid that I toggle, used as a helper when adding shapes
for aligning guidelines I want to ignore all lines in that grid
and I thought I can ignore them if I don't add them to the set that is being returned

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@Smrtnyk
I wrote the custom code according to your method and found that it works. You can compare it with the demo to see where the differences are.
custom demo

2024-11-08.10.37.59.mov

const set = new Set<FabricObject>();
const p = target.parent ?? target.canvas;
p?.getObjects().forEach((o) => {
set.add(o);
});
return set;
},
});
```

```ts
import { initAligningGuidelines } from 'fabric/extensions';

// You can customize the alignment point, the example only aligns the TL control point
initAligningGuidelines(myCanvas, {
getPointMap: function (target) {
const tl = target.getCoords().tl;
return { tl };
},
});
```

```ts
import { initAligningGuidelines } from 'fabric/extensions';

FabricObject.createControls = function () {
// custom controllers
return { controls: { abc: new Control({}) } };
};

// You can set control points for custom controllers
initAligningGuidelines(myCanvas, {
getPointMap: function (target) {
const abc = target.getCoords().tl;
return { abc };
},
getContraryMap: function (target) {
const abc = target.aCoords.br;
return { abc };
},
contraryOriginMap: {
// If abc is the top-left point, then the reference point is the bottom-right.
abc: ['right', 'bottom'],
},
});
```

```ts
import { initAligningGuidelines } from 'fabric/extensions';

// You can close all
initAligningGuidelines(myCanvas, {
closeVLine: true,
closeHLine: true,
getPointMap: function (_) {
return {};
},
});
```
6 changes: 5 additions & 1 deletion extensions/aligning_guidelines/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,9 @@ export const aligningLineConfig: AligningLineConfig = {
/** Aligning line dimensions */
width: 1,
/** Aligning line color */
color: 'rgb(255,0,0,0.9)',
color: 'rgba(255,0,0,0.9)',
/** Close Vertical line, default false. */
closeVLine: false,
/** Close horizontal line, default false. */
closeHLine: false,
};
180 changes: 113 additions & 67 deletions extensions/aligning_guidelines/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ import type {
BasicTransformEvent,
Canvas,
FabricObject,
TBBox,
TPointerEvent,
} from 'fabric';
import { Point, util } from 'fabric';
import { Point } from 'fabric';
import {
collectHorizontalPoint,
collectVerticalPoint,
Expand All @@ -15,14 +14,11 @@ import {
drawPointList,
drawVerticalLine,
} from './util/draw';
import { getObjectsByTarget } from './util/get-objects-by-target';
import { collectLine } from './util/collect-line';
import type {
AligningLineConfig,
HorizontalLine,
VerticalLine,
} from './typedefs';
import type { AligningLineConfig, LineProps } from './typedefs';
import { aligningLineConfig } from './constant';
import { getObjectsByTarget } from './util/get-objects-by-target';
import { getContraryMap, getPointMap } from './util/basic';

type TransformEvent = BasicTransformEvent<TPointerEvent> & {
target: FabricObject;
Expand All @@ -38,107 +34,157 @@ export function initAligningGuidelines(

const horizontalLines = new Set<string>();
const verticalLines = new Set<string>();
// When we drag to resize using center points like mt, ml, mb, and mr,
// we do not need to draw line segments; we only need to draw the target points.
let onlyDrawPoint = false;
const cacheMap = new Map<string, [TBBox, Point[]]>();
const cacheMap = new Map<string, Point[]>();

const getCaCheMapValue = (object: FabricObject) => {
// If there is an ID and the ID is unique, we can cache using the ID for acceleration.
// However, since Fabric does not have a built-in ID, we use the position information as the key for caching.
// const cacheKey = object.id;
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 = util.makeBoundingBoxFromPoints(coords);
const value: [TBBox, Point[]] = [rect, coords];
const value = object.getCoords();
value.push(object.getCenterPoint());
cacheMap.set(cacheKey, value);
return value;
};

function moving(e: TransformEvent) {
const activeObject = e.target;
activeObject.setCoords();
const target = e.target;
// We need to obtain the real-time coordinates of the current object, so we need to update them in real-time
target.setCoords();
onlyDrawPoint = false;
verticalLines.clear();
horizontalLines.clear();

const objects = getObjectsByTarget(activeObject);
const activeObjectRect = activeObject.getBoundingRect();
// Find the shapes associated with the current graphic to draw reference lines for it.
const objects =
options.getObjectsByTarget?.(target) ?? getObjectsByTarget(target);
const points: Point[] = [];
// Collect all the points to draw reference lines.
for (const object of objects) points.push(...getCaCheMapValue(object));

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));
});
}
// Obtain horizontal and vertical reference lines.
const { vLines, hLines } = collectLine(target, points);
vLines.forEach((o) => {
// Objects cannot be deduplicated; convert them to strings for deduplication.
verticalLines.add(JSON.stringify(o));
});
hLines.forEach((o) => {
// Objects cannot be deduplicated; convert them to strings for deduplication.
horizontalLines.add(JSON.stringify(o));
});
}

function scalingOrResizing(e: TransformEvent) {
// br bl tr tl mb ml mt mr
const activeObject = e.target;
activeObject.setCoords();
const target = e.target;
// We need to obtain the real-time coordinates of the current object, so we need to update them in real-time
target.setCoords();
// The value of action can be scaleX, scaleY, scale, resize, etc.
// If it does not start with "scale," it is considered a modification of size.
const isScale = String(e.transform.action).startsWith('scale');
verticalLines.clear();
horizontalLines.clear();

const objects = getObjectsByTarget(activeObject);
const objects =
options.getObjectsByTarget?.(target) ?? getObjectsByTarget(target);
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;
// When the shape is flipped, the tl obtained through getCoords is actually tr,
// and tl is actually tr. We need to make correction adjustments.
// tr <-> tl、 bl <-> br、 mb <-> mt、 ml <-> mr
if (target.flipX) corner = corner.replace('l', 'r').replace('r', 'l');
if (target.flipY) corner = corner.replace('t', 'b').replace('b', 't');

// Obtain the coordinates of the current operation point through the value of corner.
// users can be allowed to customize and pass in custom corners.
const pointMap = options.getPointMap?.(target) ?? getPointMap(target);
if (!(corner in pointMap)) return;
onlyDrawPoint = corner.includes('m');
if (onlyDrawPoint) {
const angle = activeObject.getTotalAngle();
const angle = target.getTotalAngle();
// When the shape is rotated, it is meaningless to draw points using the center point.
if (angle % 90 != 0) return;
index -= 4;
}
let point = activeObject.getCoords()[index];
// If manipulating tl, then when the shape changes size, it should be positioned by br,
// and the same applies to others.
// users can be allowed to customize and pass in custom corners.
const contraryMap =
options.getContraryMap?.(target) ?? getContraryMap(target);

const point = pointMap[corner];
let diagonalPoint = contraryMap[corner];
// When holding the centerKey (default is altKey), the shape will scale based on the center point, with the reference point being the center.
const isCenter = e.transform.altKey;
if (isCenter) diagonalPoint = diagonalPoint.add(point).scalarDivide(2);

const uniformIsToggled = e.e[canvas.uniScaleKey!];
let isUniform =
(canvas.uniformScaling && !uniformIsToggled) ||
(!canvas.uniformScaling && uniformIsToggled);
// When controlling through the center point,
// if isUniform is true, it actually changes the skew, so it is meaningless.
if (onlyDrawPoint) isUniform = false;

const list: Point[] = [];
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];
const d = getCaCheMapValue(object);
// When only drawing reference points, the reference shape's center point becomes irrelevant. Disable it.
const count = onlyDrawPoint ? d.length - 1 : d.length;
list.push(...d.slice(0, count));
}

const props = {
target,
point,
diagonalPoint,
corner,
list,
isScale,
isUniform,
isCenter,
};
// Obtain horizontal and vertical reference lines.
const vLines = collectVerticalPoint(props);
const hLines = collectHorizontalPoint(props);
vLines.forEach((o) => {
// Objects cannot be deduplicated; convert them to strings for deduplication.
verticalLines.add(JSON.stringify(o));
});
hLines.forEach((o) => {
// Objects cannot be deduplicated; convert them to strings for deduplication.
horizontalLines.add(JSON.stringify(o));
});
}

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));
const list: LineProps[] = [];
if (!options.closeVLine) {
for (const v of verticalLines) list.push(JSON.parse(v));
}
if (!options.closeHLine) {
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));
if (!options.closeVLine) {
for (const v of verticalLines) drawVerticalLine(canvas, JSON.parse(v));
}
if (!options.closeHLine) {
for (const h of horizontalLines) {
drawHorizontalLine(canvas, JSON.parse(h));
}
}
}
}
function mouseUp() {
Expand Down
Loading