chore: merge blocksuite source code (#9213)

This commit is contained in:
Mirone
2024-12-20 15:38:06 +08:00
committed by GitHub
parent 2c9ef916f4
commit 30200ff86d
2031 changed files with 238888 additions and 229 deletions
@@ -0,0 +1,120 @@
import type { PointTestOptions } from '@blocksuite/block-std/gfx';
import type { IBound, IVec } from '@blocksuite/global/utils';
import {
Bound,
getCenterAreaBounds,
getPointsFromBoundWithRotation,
linePolygonIntersects,
pointInPolygon,
PointLocation,
pointOnPolygonStoke,
polygonGetPointTangent,
polygonNearestPoint,
rotatePoints,
} from '@blocksuite/global/utils';
import { DEFAULT_CENTRAL_AREA_RATIO } from '../../../consts/index.js';
import type { ShapeElementModel } from '../shape.js';
export const diamond = {
points({ x, y, w, h }: IBound): IVec[] {
return [
[x, y + h / 2],
[x + w / 2, y],
[x + w, y + h / 2],
[x + w / 2, y + h],
];
},
draw(ctx: CanvasRenderingContext2D, { x, y, w, h, rotate = 0 }: IBound) {
const cx = x + w / 2;
const cy = y + h / 2;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate((rotate * Math.PI) / 180);
ctx.translate(-cx, -cy);
ctx.beginPath();
ctx.moveTo(x, y + h / 2);
ctx.lineTo(x + w / 2, y);
ctx.lineTo(x + w, y + h / 2);
ctx.lineTo(x + w / 2, y + h);
ctx.closePath();
ctx.restore();
},
includesPoint(
this: ShapeElementModel,
x: number,
y: number,
options: PointTestOptions
) {
const point: IVec = [x, y];
const points = getPointsFromBoundWithRotation(this, diamond.points);
let hit = pointOnPolygonStoke(
point,
points,
(options?.hitThreshold ?? 1) / (options.zoom ?? 1)
);
if (!hit) {
if (!options.ignoreTransparent || this.filled) {
hit = pointInPolygon([x, y], points);
} else {
// If shape is not filled or transparent
const text = this.text;
if (!text || !text.length) {
// Check the center area of the shape
const centralBounds = getCenterAreaBounds(
this,
DEFAULT_CENTRAL_AREA_RATIO
);
const centralPoints = getPointsFromBoundWithRotation(
centralBounds,
diamond.points
);
hit = pointInPolygon(point, centralPoints);
} else if (this.textBound) {
hit = pointInPolygon(
point,
getPointsFromBoundWithRotation(
this,
() => Bound.from(this.textBound!).points
)
);
}
}
}
return hit;
},
containsBound(bounds: Bound, element: ShapeElementModel) {
const points = getPointsFromBoundWithRotation(element, diamond.points);
return points.some(point => bounds.containsPoint(point));
},
getNearestPoint(point: IVec, element: ShapeElementModel) {
const points = getPointsFromBoundWithRotation(element, diamond.points);
return polygonNearestPoint(points, point);
},
getLineIntersections(start: IVec, end: IVec, element: ShapeElementModel) {
const points = getPointsFromBoundWithRotation(element, diamond.points);
return linePolygonIntersects(start, end, points);
},
getRelativePointLocation(position: IVec, element: ShapeElementModel) {
const bound = Bound.deserialize(element.xywh);
const point = bound.getRelativePoint(position);
let points = diamond.points(bound);
points.push(point);
points = rotatePoints(points, bound.center, element.rotate);
const rotatePoint = points.pop() as IVec;
const tangent = polygonGetPointTangent(points, rotatePoint);
return new PointLocation(rotatePoint, tangent);
},
};
@@ -0,0 +1,202 @@
import type { PointTestOptions } from '@blocksuite/block-std/gfx';
import {
Bound,
clamp,
getPointsFromBoundWithRotation,
type IBound,
type IVec,
lineEllipseIntersects,
pointInEllipse,
pointInPolygon,
PointLocation,
rotatePoints,
toRadian,
Vec,
} from '@blocksuite/global/utils';
import { DEFAULT_CENTRAL_AREA_RATIO } from '../../../consts/index.js';
import type { ShapeElementModel } from '../shape.js';
export const ellipse = {
points({ x, y, w, h }: IBound): IVec[] {
return [
[x, y + h / 2],
[x + w / 2, y],
[x + w, y + h / 2],
[x + w / 2, y + h],
];
},
draw(ctx: CanvasRenderingContext2D, { x, y, w, h, rotate = 0 }: IBound) {
const cx = x + w / 2;
const cy = y + h / 2;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate((rotate * Math.PI) / 180);
ctx.translate(-cx, -cy);
ctx.beginPath();
ctx.ellipse(cx, cy, w / 2, h / 2, 0, 0, 2 * Math.PI);
ctx.restore();
},
includesPoint(
this: ShapeElementModel,
x: number,
y: number,
options: PointTestOptions
) {
const point: IVec = [x, y];
const expand = (options?.hitThreshold ?? 1) / (options?.zoom ?? 1);
const rx = this.w / 2;
const ry = this.h / 2;
const center: IVec = [this.x + rx, this.y + ry];
const rad = (this.rotate * Math.PI) / 180;
let hit =
pointInEllipse(point, center, rx + expand, ry + expand, rad) &&
!pointInEllipse(point, center, rx - expand, ry - expand, rad);
if (!hit) {
if (!options.ignoreTransparent || this.filled) {
hit = pointInEllipse(point, center, rx, ry, rad);
} else {
// If shape is not filled or transparent
const text = this.text;
if (!text || !text.length) {
// Check the center area of the shape
const centralRx = rx * DEFAULT_CENTRAL_AREA_RATIO;
const centralRy = ry * DEFAULT_CENTRAL_AREA_RATIO;
hit = pointInEllipse(point, center, centralRx, centralRy, rad);
} else if (this.textBound) {
hit = pointInPolygon(
point,
getPointsFromBoundWithRotation(
this,
() => Bound.from(this.textBound!).points
)
);
}
}
}
return hit;
},
containsBound(bounds: Bound, element: ShapeElementModel): boolean {
const points = getPointsFromBoundWithRotation(element, ellipse.points);
return points.some(point => bounds.containsPoint(point));
},
// See links:
// * https://github.com/0xfaded/ellipse_demo/issues/1
// * https://blog.chatfield.io/simple-method-for-distance-to-ellipse/
// * https://gist.github.com/fundon/11331322d3ca223c42e216df48c339e1
// * https://github.com/excalidraw/excalidraw/blob/master/packages/utils/geometry/geometry.ts#L888 (MIT)
getNearestPoint(point: IVec, { rotate, xywh }: ShapeElementModel) {
const { center, w, h } = Bound.deserialize(xywh);
const rad = toRadian(rotate);
const a = w / 2;
const b = h / 2;
// Use the center of the ellipse as the origin
const [rotatedPointX, rotatedPointY] = Vec.rot(
Vec.sub(point, center),
-rad
);
const px = Math.abs(rotatedPointX);
const py = Math.abs(rotatedPointY);
let tx = Math.SQRT1_2; // 0.707
let ty = Math.SQRT1_2; // 0.707
let i = 0;
for (; i < 3; i++) {
const x = a * tx;
const y = b * ty;
const ex = ((a * a - b * b) * tx ** 3) / a;
const ey = ((b * b - a * a) * ty ** 3) / b;
const rx = x - ex;
const ry = y - ey;
const qx = px - ex;
const qy = py - ey;
const r = Math.hypot(ry, rx);
const q = Math.hypot(qy, qx);
tx = clamp(((qx * r) / q + ex) / a, 0, 1);
ty = clamp(((qy * r) / q + ey) / b, 0, 1);
const t = Math.hypot(ty, tx);
tx /= t;
ty /= t;
}
return Vec.add(
Vec.rot(
[a * tx * Math.sign(rotatedPointX), b * ty * Math.sign(rotatedPointY)],
rad
),
center
);
},
getLineIntersections(
start: IVec,
end: IVec,
{ rotate, xywh }: ShapeElementModel
) {
const rad = toRadian(rotate);
const bound = Bound.deserialize(xywh);
return lineEllipseIntersects(
start,
end,
bound.center,
bound.w / 2,
bound.h / 2,
rad
);
},
getRelativePointLocation(
relativePoint: IVec,
{ rotate, xywh }: ShapeElementModel
) {
const bounds = Bound.deserialize(xywh);
const point = bounds.getRelativePoint(relativePoint);
const { x, y, w, h, center } = bounds;
const points = rotatePoints(
[
[x, y],
[x + w / 2, y],
[x + w, y],
[x + w, y + h / 2],
[x + w, y + h],
[x + w / 2, y + h],
[x, y + h],
[x, y + h / 2],
point,
],
center,
rotate
);
const rotatedPoint = points.pop() as IVec;
const len = points.length;
let tangent: IVec = [0, 0.5];
let i = 0;
for (; i < len; i++) {
const p0 = points[i];
const p1 = points[(i + 1) % len];
const bounds = Bound.fromPoints([p0, p1, center]);
if (bounds.containsPoint(rotatedPoint)) {
tangent = Vec.normalize(Vec.sub(p1, p0));
break;
}
}
return new PointLocation(rotatedPoint, tangent);
},
};
@@ -0,0 +1,12 @@
import type { ShapeType } from '../../../consts/shape.js';
import { diamond } from './diamond.js';
import { ellipse } from './ellipse.js';
import { rect } from './rect.js';
import { triangle } from './triangle.js';
export const shapeMethods: Record<ShapeType, typeof rect> = {
rect,
triangle,
ellipse,
diamond,
};
@@ -0,0 +1,119 @@
import type { PointTestOptions } from '@blocksuite/block-std/gfx';
import type { IBound, IVec } from '@blocksuite/global/utils';
import {
Bound,
getCenterAreaBounds,
getPointsFromBoundWithRotation,
linePolygonIntersects,
pointInPolygon,
PointLocation,
pointOnPolygonStoke,
polygonGetPointTangent,
polygonNearestPoint,
rotatePoints,
} from '@blocksuite/global/utils';
import { DEFAULT_CENTRAL_AREA_RATIO } from '../../../consts/index.js';
import type { ShapeElementModel } from '../shape.js';
export const rect = {
points({ x, y, w, h }: IBound) {
return [
[x, y],
[x + w, y],
[x + w, y + h],
[x, y + h],
];
},
draw(ctx: CanvasRenderingContext2D, { x, y, w, h, rotate = 0 }: IBound) {
ctx.save();
ctx.translate(x + w / 2, y + h / 2);
ctx.rotate((rotate * Math.PI) / 180);
ctx.translate(-x - w / 2, -y - h / 2);
ctx.rect(x, y, w, h);
ctx.restore();
},
includesPoint(
this: ShapeElementModel,
x: number,
y: number,
options: PointTestOptions
) {
const point: IVec = [x, y];
const points = getPointsFromBoundWithRotation(
this,
undefined,
options.responsePadding
);
let hit = pointOnPolygonStoke(
point,
points,
(options?.hitThreshold ?? 1) / (options.zoom ?? 1)
);
if (!hit) {
// If the point is not on the stroke, check if it is in the shape
// When the shape is filled and transparent is not ignored
if (!options.ignoreTransparent || this.filled) {
hit = pointInPolygon([x, y], points);
} else {
// If shape is not filled or transparent
// Check if hit the text area
const text = this.text;
if (!text || !text.length) {
// if not, check the default center area of the shape
const centralBounds = getCenterAreaBounds(
this,
DEFAULT_CENTRAL_AREA_RATIO
);
const centralPoints = getPointsFromBoundWithRotation(centralBounds);
// Check if the point is in the center area
hit = pointInPolygon([x, y], centralPoints);
} else if (this.textBound) {
hit = pointInPolygon(
point,
getPointsFromBoundWithRotation(
this,
() => Bound.from(this.textBound!).points
)
);
}
}
}
return hit;
},
containsBound(bounds: Bound, element: ShapeElementModel): boolean {
const points = getPointsFromBoundWithRotation(element);
return points.some(point => bounds.containsPoint(point));
},
getNearestPoint(point: IVec, element: ShapeElementModel) {
const points = getPointsFromBoundWithRotation(element);
return polygonNearestPoint(points, point);
},
getLineIntersections(start: IVec, end: IVec, element: ShapeElementModel) {
const points = getPointsFromBoundWithRotation(element);
return linePolygonIntersects(start, end, points);
},
getRelativePointLocation(relativePoint: IVec, element: ShapeElementModel) {
const bound = Bound.deserialize(element.xywh);
const point = bound.getRelativePoint(relativePoint);
const rotatePoint = rotatePoints(
[point],
bound.center,
element.rotate ?? 0
)[0];
const points = rotatePoints(
bound.points,
bound.center,
element.rotate ?? 0
);
const tangent = polygonGetPointTangent(points, rotatePoint);
return new PointLocation(rotatePoint, tangent);
},
};
@@ -0,0 +1,116 @@
import type { PointTestOptions } from '@blocksuite/block-std/gfx';
import type { IBound, IVec } from '@blocksuite/global/utils';
import {
Bound,
getCenterAreaBounds,
getPointsFromBoundWithRotation,
linePolygonIntersects,
pointInPolygon,
PointLocation,
pointOnPolygonStoke,
polygonGetPointTangent,
polygonNearestPoint,
rotatePoints,
} from '@blocksuite/global/utils';
import { DEFAULT_CENTRAL_AREA_RATIO } from '../../../consts/index.js';
import type { ShapeElementModel } from '../shape.js';
export const triangle = {
points({ x, y, w, h }: IBound): IVec[] {
return [
[x, y + h],
[x + w / 2, y],
[x + w, y + h],
];
},
draw(ctx: CanvasRenderingContext2D, { x, y, w, h, rotate = 0 }: IBound) {
const cx = x + w / 2;
const cy = y + h / 2;
ctx.save();
ctx.translate(cx, cy);
ctx.rotate((rotate * Math.PI) / 180);
ctx.translate(-cx, -cy);
ctx.beginPath();
ctx.moveTo(x, y + h);
ctx.lineTo(x + w / 2, y);
ctx.lineTo(x + w, y + h);
ctx.closePath();
ctx.restore();
},
includesPoint(
this: ShapeElementModel,
x: number,
y: number,
options: PointTestOptions
) {
const point: IVec = [x, y];
const points = getPointsFromBoundWithRotation(this, triangle.points);
let hit = pointOnPolygonStoke(
point,
points,
(options?.hitThreshold ?? 1) / (options?.zoom ?? 1)
);
if (!hit) {
if (!options.ignoreTransparent || this.filled) {
hit = pointInPolygon([x, y], points);
} else {
// If shape is not filled or transparent
const text = this.text;
if (!text || !text.length) {
// Check the center area of the shape
const centralBounds = getCenterAreaBounds(
this,
DEFAULT_CENTRAL_AREA_RATIO
);
const centralPoints = getPointsFromBoundWithRotation(
centralBounds,
triangle.points
);
hit = pointInPolygon([x, y], centralPoints);
} else if (this.textBound) {
hit = pointInPolygon(
point,
getPointsFromBoundWithRotation(
this,
() => Bound.from(this.textBound!).points
)
);
}
}
}
return hit;
},
containsBound(bounds: Bound, element: ShapeElementModel): boolean {
const points = getPointsFromBoundWithRotation(element, triangle.points);
return points.some(point => bounds.containsPoint(point));
},
getNearestPoint(point: IVec, element: ShapeElementModel) {
const points = getPointsFromBoundWithRotation(element, triangle.points);
return polygonNearestPoint(points, point);
},
getLineIntersections(start: IVec, end: IVec, element: ShapeElementModel) {
const points = getPointsFromBoundWithRotation(element, triangle.points);
return linePolygonIntersects(start, end, points);
},
getRelativePointLocation(position: IVec, element: ShapeElementModel) {
const bound = Bound.deserialize(element.xywh);
const point = bound.getRelativePoint(position);
let points = triangle.points(bound);
points.push(point);
points = rotatePoints(points, bound.center, element.rotate);
const rotatePoint = points.pop() as IVec;
const tangent = polygonGetPointTangent(points, rotatePoint);
return new PointLocation(rotatePoint, tangent);
},
};