mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-21 12:06:35 +08:00
302 lines
8.4 KiB
TypeScript
302 lines
8.4 KiB
TypeScript
import { Overlay } from '@blocksuite/affine-block-surface';
|
|
import { PathGenerator } from '@blocksuite/affine-gfx-connector';
|
|
import {
|
|
ConnectorMode,
|
|
LayoutType,
|
|
type MindmapElementModel,
|
|
type MindmapNode,
|
|
} from '@blocksuite/affine-model';
|
|
import { ThemeProvider } from '@blocksuite/affine-shared/services';
|
|
import {
|
|
type Bound,
|
|
isVecZero,
|
|
type IVec,
|
|
PointLocation,
|
|
toRadian,
|
|
Vec,
|
|
} from '@blocksuite/global/gfx';
|
|
import last from 'lodash-es/last';
|
|
|
|
import { NODE_HORIZONTAL_SPACING, NODE_VERTICAL_SPACING } from '../view/layout';
|
|
|
|
export class MindMapIndicatorOverlay extends Overlay {
|
|
static INDICATOR_SIZE = [48, 22];
|
|
|
|
static override overlayName: string = 'mindmap-indicator';
|
|
|
|
currentDragPos: IVec | null = null;
|
|
|
|
direction: LayoutType.LEFT | LayoutType.RIGHT = LayoutType.RIGHT;
|
|
|
|
dragNodeImage: HTMLCanvasElement | null = null;
|
|
|
|
dragNodePos: IVec = [0, 0];
|
|
|
|
mode: ConnectorMode = ConnectorMode.Straight;
|
|
|
|
parentBound: Bound | null = null;
|
|
|
|
pathGen = new PathGenerator();
|
|
|
|
targetBound: Bound | null = null;
|
|
|
|
get themeService() {
|
|
return this.gfx.std.get(ThemeProvider);
|
|
}
|
|
|
|
private _generatePath() {
|
|
const startRelativePos =
|
|
this.direction === LayoutType.RIGHT
|
|
? PointLocation.fromVec([1, 0.5])
|
|
: PointLocation.fromVec([0, 0.5]);
|
|
const endRelativePos =
|
|
this.direction === LayoutType.RIGHT
|
|
? PointLocation.fromVec([0, 0.5])
|
|
: PointLocation.fromVec([1, 0.5]);
|
|
const { parentBound, targetBound: newPosBound } = this;
|
|
|
|
if (this.mode === ConnectorMode.Orthogonal) {
|
|
return this.pathGen
|
|
.generateOrthogonalConnectorPath({
|
|
startPoint: this._getRelativePoint(parentBound!, startRelativePos),
|
|
endPoint: this._getRelativePoint(newPosBound!, endRelativePos),
|
|
startBound: parentBound,
|
|
endBound: newPosBound,
|
|
})
|
|
.map(p => new PointLocation(p));
|
|
} else if (this.mode === ConnectorMode.Curve) {
|
|
const startPoint = this._getRelativePoint(
|
|
this.parentBound!,
|
|
startRelativePos
|
|
);
|
|
const endPoint = this._getRelativePoint(
|
|
this.targetBound!,
|
|
endRelativePos
|
|
);
|
|
|
|
const startTangentVertical = Vec.rot(startPoint.tangent, -Math.PI / 2);
|
|
startPoint.out = Vec.mul(
|
|
startTangentVertical,
|
|
Math.max(
|
|
100,
|
|
Math.abs(
|
|
Vec.pry(Vec.sub(endPoint, startPoint), startTangentVertical)
|
|
) / 3
|
|
)
|
|
);
|
|
|
|
const endTangentVertical = Vec.rot(endPoint.tangent, -Math.PI / 2);
|
|
endPoint.in = Vec.mul(
|
|
endTangentVertical,
|
|
Math.max(
|
|
100,
|
|
Math.abs(Vec.pry(Vec.sub(startPoint, endPoint), endTangentVertical)) /
|
|
3
|
|
)
|
|
);
|
|
|
|
return [startPoint, endPoint];
|
|
} else {
|
|
const startPoint = new PointLocation(
|
|
this.parentBound!.getRelativePoint(startRelativePos)
|
|
);
|
|
const endPoint = new PointLocation(
|
|
this.targetBound!.getRelativePoint(endRelativePos)
|
|
);
|
|
|
|
return [startPoint, endPoint];
|
|
}
|
|
}
|
|
|
|
private _getRelativePoint(bound: Bound, position: IVec) {
|
|
const location = new PointLocation(
|
|
bound.getRelativePoint(position as IVec)
|
|
);
|
|
|
|
if (isVecZero(Vec.sub(position, [0, 0.5])))
|
|
location.tangent = Vec.rot([0, -1], toRadian(0));
|
|
else if (isVecZero(Vec.sub(position, [1, 0.5])))
|
|
location.tangent = Vec.rot([0, 1], toRadian(0));
|
|
else if (isVecZero(Vec.sub(position, [0.5, 0])))
|
|
location.tangent = Vec.rot([1, 0], toRadian(0));
|
|
else if (isVecZero(Vec.sub(position, [0.5, 1])))
|
|
location.tangent = Vec.rot([-1, 0], toRadian(0));
|
|
|
|
return location;
|
|
}
|
|
|
|
/**
|
|
* Use to calculate the position of the indicator given its sibling's bound
|
|
* @param siblingBound
|
|
* @param direction
|
|
*/
|
|
private _moveRelativeToBound(
|
|
siblingBound: Bound,
|
|
direction: 'up' | 'down',
|
|
layoutDir: Exclude<LayoutType, LayoutType.BALANCE>
|
|
) {
|
|
const isLeftLayout = layoutDir === LayoutType.LEFT;
|
|
const isUpDirection = direction === 'up';
|
|
|
|
return siblingBound.moveDelta(
|
|
isLeftLayout
|
|
? siblingBound.w - MindMapIndicatorOverlay.INDICATOR_SIZE[0]
|
|
: 0,
|
|
isUpDirection
|
|
? -(
|
|
NODE_VERTICAL_SPACING / 2 +
|
|
MindMapIndicatorOverlay.INDICATOR_SIZE[1] / 2
|
|
)
|
|
: siblingBound.h +
|
|
NODE_VERTICAL_SPACING / 2 -
|
|
MindMapIndicatorOverlay.INDICATOR_SIZE[1] / 2
|
|
);
|
|
}
|
|
|
|
override clear() {
|
|
this.targetBound = null;
|
|
this.parentBound = null;
|
|
}
|
|
|
|
override render(ctx: CanvasRenderingContext2D): void {
|
|
if (this.currentDragPos && this.dragNodeImage) {
|
|
ctx.save();
|
|
ctx.globalAlpha = 0.3;
|
|
ctx.drawImage(
|
|
this.dragNodeImage,
|
|
this.currentDragPos[0] + this.dragNodePos[0],
|
|
this.currentDragPos[1] + this.dragNodePos[1],
|
|
this.dragNodeImage.width / 2,
|
|
this.dragNodeImage.height / 2
|
|
);
|
|
ctx.restore();
|
|
}
|
|
|
|
if (!this.parentBound || !this.targetBound) {
|
|
return;
|
|
}
|
|
|
|
const targetPos = this.targetBound;
|
|
const points = this._generatePath();
|
|
const color = this.themeService.getColorValue(
|
|
'--affine-primary-color',
|
|
'#1E96EB',
|
|
true
|
|
);
|
|
|
|
ctx.strokeStyle = color;
|
|
ctx.fillStyle = color;
|
|
ctx.lineWidth = 3;
|
|
|
|
ctx.beginPath();
|
|
ctx.roundRect(targetPos.x, targetPos.y, targetPos.w, targetPos.h, 4);
|
|
ctx.fill();
|
|
|
|
ctx.beginPath();
|
|
ctx.moveTo(points[0][0], points[0][1]);
|
|
|
|
if (this.mode === ConnectorMode.Curve) {
|
|
points.forEach((point, idx) => {
|
|
if (idx === 0) return;
|
|
const last = points[idx - 1];
|
|
ctx.bezierCurveTo(
|
|
last.absOut[0],
|
|
last.absOut[1],
|
|
point.absIn[0],
|
|
point.absIn[1],
|
|
point[0],
|
|
point[1]
|
|
);
|
|
});
|
|
} else {
|
|
points.forEach((point, idx) => {
|
|
if (idx === 0) return;
|
|
ctx.lineTo(point[0], point[1]);
|
|
});
|
|
}
|
|
|
|
ctx.stroke();
|
|
ctx.closePath();
|
|
}
|
|
|
|
setIndicatorInfo(options: {
|
|
targetMindMap: MindmapElementModel;
|
|
target: MindmapNode;
|
|
parent: MindmapNode;
|
|
parentChildren: MindmapNode[];
|
|
insertPosition:
|
|
| {
|
|
type: 'sibling';
|
|
layoutDir: Exclude<LayoutType, LayoutType.BALANCE>;
|
|
position: 'prev' | 'next';
|
|
}
|
|
| { type: 'child'; layoutDir: Exclude<LayoutType, LayoutType.BALANCE> };
|
|
path: number[];
|
|
}) {
|
|
const {
|
|
insertPosition,
|
|
parent,
|
|
parentChildren,
|
|
targetMindMap,
|
|
target,
|
|
path,
|
|
} = options;
|
|
|
|
const parentBound = parent.element.elementBound;
|
|
const isBalancedMindMap = targetMindMap.layoutType === LayoutType.BALANCE;
|
|
const isLeftLayout = insertPosition.layoutDir === LayoutType.LEFT;
|
|
const isFirstLevel = path.length === 2;
|
|
|
|
this.direction = insertPosition.layoutDir;
|
|
this.parentBound = parentBound;
|
|
|
|
if (insertPosition.type === 'sibling') {
|
|
const targetBound = target.element.elementBound;
|
|
|
|
this.targetBound =
|
|
isBalancedMindMap && isFirstLevel && isLeftLayout
|
|
? this._moveRelativeToBound(
|
|
targetBound,
|
|
insertPosition.position === 'next' ? 'up' : 'down',
|
|
insertPosition.layoutDir
|
|
)
|
|
: this._moveRelativeToBound(
|
|
targetBound,
|
|
insertPosition.position === 'next' ? 'down' : 'up',
|
|
insertPosition.layoutDir
|
|
);
|
|
} else {
|
|
if (parentChildren.length === 0 || parent.detail.collapsed) {
|
|
this.targetBound = parentBound.moveDelta(
|
|
(isLeftLayout ? -1 : 1) *
|
|
(NODE_HORIZONTAL_SPACING / 2 + parentBound.w),
|
|
parentBound.h / 2 - MindMapIndicatorOverlay.INDICATOR_SIZE[1] / 2
|
|
);
|
|
} else {
|
|
const lastChildBound = last(parentChildren)!.element.elementBound;
|
|
|
|
this.targetBound =
|
|
isBalancedMindMap && isFirstLevel && isLeftLayout
|
|
? this._moveRelativeToBound(
|
|
lastChildBound,
|
|
'up',
|
|
insertPosition.layoutDir
|
|
)
|
|
: this._moveRelativeToBound(
|
|
lastChildBound,
|
|
'down',
|
|
insertPosition.layoutDir
|
|
);
|
|
}
|
|
}
|
|
|
|
this.targetBound.w = MindMapIndicatorOverlay.INDICATOR_SIZE[0];
|
|
this.targetBound.h = MindMapIndicatorOverlay.INDICATOR_SIZE[1];
|
|
|
|
this.mode = targetMindMap.styleGetter.getNodeStyle(
|
|
target,
|
|
options.path
|
|
).connector.mode;
|
|
}
|
|
}
|