mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-07 01:09:54 +08:00
refactor(editor): separate the element renders (#11461)
This commit is contained in:
@@ -0,0 +1,41 @@
|
||||
import {
|
||||
type ElementRenderer,
|
||||
ElementRendererExtension,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
DefaultTheme,
|
||||
type HighlighterElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
|
||||
export const highlighter: ElementRenderer<HighlighterElementModel> = (
|
||||
model,
|
||||
ctx,
|
||||
matrix,
|
||||
renderer
|
||||
) => {
|
||||
const {
|
||||
rotate,
|
||||
deserializedXYWH: [, , w, h],
|
||||
} = model;
|
||||
const cx = w / 2;
|
||||
const cy = h / 2;
|
||||
|
||||
ctx.setTransform(
|
||||
matrix.translateSelf(cx, cy).rotateSelf(rotate).translateSelf(-cx, -cy)
|
||||
);
|
||||
|
||||
const color = renderer.getColorValue(
|
||||
model.color,
|
||||
DefaultTheme.hightlighterColor,
|
||||
true
|
||||
);
|
||||
|
||||
ctx.fillStyle = color;
|
||||
|
||||
ctx.fill(new Path2D(model.commands));
|
||||
};
|
||||
|
||||
export const HighlighterElementRendererExtension = ElementRendererExtension(
|
||||
'highlighter',
|
||||
highlighter
|
||||
);
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './highlighter';
|
||||
export * from './shape';
|
||||
@@ -0,0 +1,66 @@
|
||||
import type {
|
||||
CanvasRenderer,
|
||||
RoughCanvas,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type {
|
||||
LocalShapeElementModel,
|
||||
ShapeElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
|
||||
import { type Colors, drawGeneralShape } from './utils.js';
|
||||
|
||||
export function diamond(
|
||||
model: ShapeElementModel | LocalShapeElementModel,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
matrix: DOMMatrix,
|
||||
renderer: CanvasRenderer,
|
||||
rc: RoughCanvas,
|
||||
colors: Colors
|
||||
) {
|
||||
const {
|
||||
seed,
|
||||
strokeWidth,
|
||||
filled,
|
||||
strokeStyle,
|
||||
roughness,
|
||||
rotate,
|
||||
shapeStyle,
|
||||
} = model;
|
||||
const [, , w, h] = model.deserializedXYWH;
|
||||
const renderOffset = Math.max(strokeWidth, 0) / 2;
|
||||
const renderWidth = w - renderOffset * 2;
|
||||
const renderHeight = h - renderOffset * 2;
|
||||
const cx = renderWidth / 2;
|
||||
const cy = renderHeight / 2;
|
||||
|
||||
const { fillColor, strokeColor } = colors;
|
||||
|
||||
ctx.setTransform(
|
||||
matrix
|
||||
.translateSelf(renderOffset, renderOffset)
|
||||
.translateSelf(cx, cy)
|
||||
.rotateSelf(rotate)
|
||||
.translateSelf(-cx, -cy)
|
||||
);
|
||||
|
||||
if (shapeStyle === 'General') {
|
||||
drawGeneralShape(ctx, model, renderer, filled, fillColor, strokeColor);
|
||||
} else {
|
||||
rc.polygon(
|
||||
[
|
||||
[renderWidth / 2, 0],
|
||||
[renderWidth, renderHeight / 2],
|
||||
[renderWidth / 2, renderHeight],
|
||||
[0, renderHeight / 2],
|
||||
],
|
||||
{
|
||||
seed,
|
||||
roughness: shapeStyle === 'Scribbled' ? roughness : 0,
|
||||
strokeLineDash: strokeStyle === 'dash' ? [12, 12] : undefined,
|
||||
stroke: strokeStyle === 'none' ? 'none' : strokeColor,
|
||||
strokeWidth,
|
||||
fill: filled ? fillColor : undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type {
|
||||
CanvasRenderer,
|
||||
RoughCanvas,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type {
|
||||
LocalShapeElementModel,
|
||||
ShapeElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
|
||||
import { type Colors, drawGeneralShape } from './utils.js';
|
||||
|
||||
export function ellipse(
|
||||
model: ShapeElementModel | LocalShapeElementModel,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
matrix: DOMMatrix,
|
||||
renderer: CanvasRenderer,
|
||||
rc: RoughCanvas,
|
||||
colors: Colors
|
||||
) {
|
||||
const {
|
||||
seed,
|
||||
strokeWidth,
|
||||
filled,
|
||||
strokeStyle,
|
||||
roughness,
|
||||
rotate,
|
||||
shapeStyle,
|
||||
} = model;
|
||||
const [, , w, h] = model.deserializedXYWH;
|
||||
const renderOffset = Math.max(strokeWidth, 0) / 2;
|
||||
const renderWidth = Math.max(1, w - renderOffset * 2);
|
||||
const renderHeight = Math.max(1, h - renderOffset * 2);
|
||||
const cx = renderWidth / 2;
|
||||
const cy = renderHeight / 2;
|
||||
|
||||
const { fillColor, strokeColor } = colors;
|
||||
|
||||
ctx.setTransform(
|
||||
matrix
|
||||
.translateSelf(renderOffset, renderOffset)
|
||||
.translateSelf(cx, cy)
|
||||
.rotateSelf(rotate)
|
||||
.translateSelf(-cx, -cy)
|
||||
);
|
||||
|
||||
if (shapeStyle === 'General') {
|
||||
drawGeneralShape(ctx, model, renderer, filled, fillColor, strokeColor);
|
||||
} else {
|
||||
rc.ellipse(cx, cy, renderWidth, renderHeight, {
|
||||
seed,
|
||||
roughness: shapeStyle === 'Scribbled' ? roughness : 0,
|
||||
strokeLineDash: strokeStyle === 'dash' ? [12, 12] : undefined,
|
||||
stroke: strokeStyle === 'none' ? 'none' : strokeColor,
|
||||
strokeWidth,
|
||||
fill: filled ? fillColor : undefined,
|
||||
curveFitting: 1,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
import {
|
||||
type CanvasRenderer,
|
||||
type ElementRenderer,
|
||||
ElementRendererExtension,
|
||||
type RoughCanvas,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
getFontMetrics,
|
||||
getFontString,
|
||||
getLineWidth,
|
||||
isRTL,
|
||||
measureTextInDOM,
|
||||
wrapTextDeltas,
|
||||
} from '@blocksuite/affine-gfx-text';
|
||||
import type {
|
||||
LocalShapeElementModel,
|
||||
ShapeElementModel,
|
||||
ShapeType,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { DefaultTheme, TextAlign } from '@blocksuite/affine-model';
|
||||
import type { IBound } from '@blocksuite/global/gfx';
|
||||
import { Bound } from '@blocksuite/global/gfx';
|
||||
import { deltaInsertsToChunks } from '@blocksuite/std/inline';
|
||||
|
||||
import { diamond } from './diamond.js';
|
||||
import { ellipse } from './ellipse.js';
|
||||
import { rect } from './rect.js';
|
||||
import { triangle } from './triangle.js';
|
||||
import { type Colors, horizontalOffset, verticalOffset } from './utils.js';
|
||||
|
||||
const shapeRenderers: Record<
|
||||
ShapeType,
|
||||
(
|
||||
model: ShapeElementModel | LocalShapeElementModel,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
matrix: DOMMatrix,
|
||||
renderer: CanvasRenderer,
|
||||
rc: RoughCanvas,
|
||||
colors: Colors
|
||||
) => void
|
||||
> = {
|
||||
diamond,
|
||||
rect,
|
||||
triangle,
|
||||
ellipse,
|
||||
};
|
||||
|
||||
export const shape: ElementRenderer<ShapeElementModel> = (
|
||||
model,
|
||||
ctx,
|
||||
matrix,
|
||||
renderer,
|
||||
rc
|
||||
) => {
|
||||
const color = renderer.getColorValue(
|
||||
model.color,
|
||||
DefaultTheme.shapeTextColor,
|
||||
true
|
||||
);
|
||||
const fillColor = renderer.getColorValue(
|
||||
model.fillColor,
|
||||
DefaultTheme.shapeFillColor,
|
||||
true
|
||||
);
|
||||
const strokeColor = renderer.getColorValue(
|
||||
model.strokeColor,
|
||||
DefaultTheme.shapeStrokeColor,
|
||||
true
|
||||
);
|
||||
const colors = { color, fillColor, strokeColor };
|
||||
|
||||
shapeRenderers[model.shapeType](model, ctx, matrix, renderer, rc, colors);
|
||||
|
||||
if (model.textDisplay) {
|
||||
renderText(model, ctx, colors);
|
||||
}
|
||||
};
|
||||
|
||||
export const ShapeElementRendererExtension = ElementRendererExtension(
|
||||
'shape',
|
||||
shape
|
||||
);
|
||||
|
||||
export * from './utils';
|
||||
|
||||
function renderText(
|
||||
model: ShapeElementModel | LocalShapeElementModel,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{ color }: Colors
|
||||
) {
|
||||
const {
|
||||
x,
|
||||
y,
|
||||
text,
|
||||
fontSize,
|
||||
fontFamily,
|
||||
fontWeight,
|
||||
textAlign,
|
||||
w,
|
||||
h,
|
||||
textVerticalAlign,
|
||||
padding,
|
||||
} = model;
|
||||
if (!text) return;
|
||||
|
||||
const [verticalPadding, horPadding] = padding;
|
||||
const font = getFontString(model);
|
||||
const { lineGap, lineHeight } = measureTextInDOM(
|
||||
fontFamily,
|
||||
fontSize,
|
||||
fontWeight
|
||||
);
|
||||
const metrics = getFontMetrics(fontFamily, fontSize, fontWeight);
|
||||
const lines =
|
||||
typeof text === 'string'
|
||||
? [text.split('\n').map(line => ({ insert: line }))]
|
||||
: deltaInsertsToChunks(wrapTextDeltas(text, font, w - horPadding * 2));
|
||||
const horOffset = horizontalOffset(model.w, model.textAlign, horPadding);
|
||||
const vertOffset =
|
||||
verticalOffset(
|
||||
lines,
|
||||
lineHeight + lineGap,
|
||||
h,
|
||||
textVerticalAlign,
|
||||
verticalPadding
|
||||
) +
|
||||
metrics.fontBoundingBoxAscent +
|
||||
lineGap / 2;
|
||||
let maxLineWidth = 0;
|
||||
|
||||
ctx.font = font;
|
||||
ctx.fillStyle = color;
|
||||
ctx.textAlign = textAlign;
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
|
||||
for (const [lineIndex, line] of lines.entries()) {
|
||||
for (const delta of line) {
|
||||
const str = delta.insert;
|
||||
const rtl = isRTL(str);
|
||||
const shouldTemporarilyAttach = rtl && !ctx.canvas.isConnected;
|
||||
if (shouldTemporarilyAttach) {
|
||||
// to correctly render RTL text mixed with LTR, we have to append it
|
||||
// to the DOM
|
||||
document.body.append(ctx.canvas);
|
||||
}
|
||||
|
||||
if (ctx.canvas.dir !== (rtl ? 'rtl' : 'ltr')) {
|
||||
ctx.canvas.setAttribute('dir', rtl ? 'rtl' : 'ltr');
|
||||
}
|
||||
|
||||
ctx.fillText(
|
||||
str,
|
||||
// 0.5 is the dom editor padding to make the text align with the DOM text
|
||||
horOffset + 0.5,
|
||||
lineIndex * lineHeight + vertOffset
|
||||
);
|
||||
|
||||
maxLineWidth = Math.max(maxLineWidth, getLineWidth(str, font));
|
||||
|
||||
if (shouldTemporarilyAttach) {
|
||||
ctx.canvas.remove();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const offsetX =
|
||||
model.textAlign === TextAlign.Center
|
||||
? (w - maxLineWidth) / 2
|
||||
: model.textAlign === TextAlign.Left
|
||||
? horOffset
|
||||
: horOffset - maxLineWidth;
|
||||
const offsetY = vertOffset - lineHeight + verticalPadding / 2;
|
||||
|
||||
const bound = new Bound(
|
||||
x + offsetX,
|
||||
y + offsetY,
|
||||
maxLineWidth,
|
||||
lineHeight * lines.length
|
||||
) as IBound;
|
||||
|
||||
bound.rotate = model.rotate ?? 0;
|
||||
model.textBound = bound;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import type {
|
||||
CanvasRenderer,
|
||||
RoughCanvas,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type {
|
||||
LocalShapeElementModel,
|
||||
ShapeElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
|
||||
import { type Colors, drawGeneralShape } from './utils.js';
|
||||
|
||||
/**
|
||||
* "magic number" for bezier approximations of arcs (http://itc.ktu.lt/itc354/Riskus354.pdf)
|
||||
*/
|
||||
const K_RECT = 1 - 0.5522847498;
|
||||
|
||||
export function rect(
|
||||
model: ShapeElementModel | LocalShapeElementModel,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
matrix: DOMMatrix,
|
||||
renderer: CanvasRenderer,
|
||||
rc: RoughCanvas,
|
||||
colors: Colors
|
||||
) {
|
||||
const {
|
||||
filled,
|
||||
radius,
|
||||
rotate,
|
||||
roughness,
|
||||
seed,
|
||||
shapeStyle,
|
||||
strokeStyle,
|
||||
strokeWidth,
|
||||
} = model;
|
||||
const [, , w, h] = model.deserializedXYWH;
|
||||
const renderOffset = Math.max(strokeWidth, 0) / 2;
|
||||
const renderWidth = w - renderOffset * 2;
|
||||
const renderHeight = h - renderOffset * 2;
|
||||
const r =
|
||||
radius < 1 ? Math.min(renderWidth * radius, renderHeight * radius) : radius;
|
||||
const cx = renderWidth / 2;
|
||||
const cy = renderHeight / 2;
|
||||
|
||||
const { fillColor, strokeColor } = colors;
|
||||
|
||||
ctx.setTransform(
|
||||
matrix
|
||||
.translateSelf(renderOffset, renderOffset)
|
||||
.translateSelf(cx, cy)
|
||||
.rotateSelf(rotate)
|
||||
.translateSelf(-cx, -cy)
|
||||
);
|
||||
|
||||
if (shapeStyle === 'General') {
|
||||
drawGeneralShape(ctx, model, renderer, filled, fillColor, strokeColor);
|
||||
} else {
|
||||
rc.path(
|
||||
`
|
||||
M ${r} 0
|
||||
L ${renderWidth - r} 0
|
||||
C ${renderWidth - K_RECT * r} 0 ${renderWidth} ${
|
||||
K_RECT * r
|
||||
} ${renderWidth} ${r}
|
||||
L ${renderWidth} ${renderHeight - r}
|
||||
C ${renderWidth} ${renderHeight - K_RECT * r} ${
|
||||
renderWidth - K_RECT * r
|
||||
} ${renderHeight} ${renderWidth - r} ${renderHeight}
|
||||
L ${r} ${renderHeight}
|
||||
C ${K_RECT * r} ${renderHeight} 0 ${renderHeight - K_RECT * r} 0 ${
|
||||
renderHeight - r
|
||||
}
|
||||
L 0 ${r}
|
||||
C 0 ${K_RECT * r} ${K_RECT * r} 0 ${r} 0
|
||||
Z
|
||||
`,
|
||||
{
|
||||
seed,
|
||||
roughness,
|
||||
strokeLineDash: strokeStyle === 'dash' ? [12, 12] : undefined,
|
||||
stroke: strokeStyle === 'none' ? 'none' : strokeColor,
|
||||
strokeWidth,
|
||||
fill: filled ? fillColor : undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
ctx.setTransform(
|
||||
ctx
|
||||
.getTransform()
|
||||
.translateSelf(cx, cy)
|
||||
.rotateSelf(-rotate)
|
||||
.translateSelf(-cx, -cy)
|
||||
.translateSelf(-renderOffset, -renderOffset)
|
||||
.translateSelf(cx, cy)
|
||||
.rotateSelf(rotate)
|
||||
.translateSelf(-cx, -cy)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import type {
|
||||
CanvasRenderer,
|
||||
RoughCanvas,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import type {
|
||||
LocalShapeElementModel,
|
||||
ShapeElementModel,
|
||||
} from '@blocksuite/affine-model';
|
||||
|
||||
import { type Colors, drawGeneralShape } from './utils.js';
|
||||
|
||||
export function triangle(
|
||||
model: ShapeElementModel | LocalShapeElementModel,
|
||||
ctx: CanvasRenderingContext2D,
|
||||
matrix: DOMMatrix,
|
||||
renderer: CanvasRenderer,
|
||||
rc: RoughCanvas,
|
||||
colors: Colors
|
||||
) {
|
||||
const {
|
||||
seed,
|
||||
strokeWidth,
|
||||
filled,
|
||||
strokeStyle,
|
||||
roughness,
|
||||
rotate,
|
||||
shapeStyle,
|
||||
} = model;
|
||||
const [, , w, h] = model.deserializedXYWH;
|
||||
const renderOffset = Math.max(strokeWidth, 0) / 2;
|
||||
const renderWidth = w - renderOffset * 2;
|
||||
const renderHeight = h - renderOffset * 2;
|
||||
const cx = renderWidth / 2;
|
||||
const cy = renderHeight / 2;
|
||||
|
||||
const { fillColor, strokeColor } = colors;
|
||||
|
||||
ctx.setTransform(
|
||||
matrix
|
||||
.translateSelf(renderOffset, renderOffset)
|
||||
.translateSelf(cx, cy)
|
||||
.rotateSelf(rotate)
|
||||
.translateSelf(-cx, -cy)
|
||||
);
|
||||
|
||||
if (shapeStyle === 'General') {
|
||||
drawGeneralShape(ctx, model, renderer, filled, fillColor, strokeColor);
|
||||
} else {
|
||||
rc.polygon(
|
||||
[
|
||||
[renderWidth / 2, 0],
|
||||
[renderWidth, renderHeight],
|
||||
[0, renderHeight],
|
||||
],
|
||||
{
|
||||
seed,
|
||||
roughness: shapeStyle === 'Scribbled' ? roughness : 0,
|
||||
strokeLineDash: strokeStyle === 'dash' ? [12, 12] : undefined,
|
||||
stroke: strokeStyle === 'none' ? 'none' : strokeColor,
|
||||
strokeWidth,
|
||||
fill: filled ? fillColor : undefined,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
import type { CanvasRenderer } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
getFontString,
|
||||
getLineHeight,
|
||||
getLineWidth,
|
||||
getTextWidth,
|
||||
measureTextInDOM,
|
||||
type TextDelta,
|
||||
wrapText,
|
||||
wrapTextDeltas,
|
||||
} from '@blocksuite/affine-gfx-text';
|
||||
import type {
|
||||
LocalShapeElementModel,
|
||||
ShapeElementModel,
|
||||
TextAlign,
|
||||
TextVerticalAlign,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { FeatureFlagService } from '@blocksuite/affine-shared/services';
|
||||
import type { Bound, SerializedXYWH } from '@blocksuite/global/gfx';
|
||||
import { deltaInsertsToChunks } from '@blocksuite/std/inline';
|
||||
|
||||
export type Colors = {
|
||||
color: string;
|
||||
fillColor: string;
|
||||
strokeColor: string;
|
||||
};
|
||||
|
||||
export function drawGeneralShape(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
shapeModel: ShapeElementModel | LocalShapeElementModel,
|
||||
renderer: CanvasRenderer,
|
||||
filled: boolean,
|
||||
fillColor: string,
|
||||
strokeColor: string
|
||||
) {
|
||||
const sizeOffset = Math.max(shapeModel.strokeWidth, 0);
|
||||
const w = Math.max(shapeModel.w - sizeOffset, 0);
|
||||
const h = Math.max(shapeModel.h - sizeOffset, 0);
|
||||
|
||||
switch (shapeModel.shapeType) {
|
||||
case 'rect':
|
||||
drawRect(ctx, 0, 0, w, h, shapeModel.radius ?? 0);
|
||||
break;
|
||||
case 'diamond':
|
||||
drawDiamond(ctx, 0, 0, w, h);
|
||||
break;
|
||||
case 'ellipse':
|
||||
drawEllipse(ctx, 0, 0, w, h);
|
||||
break;
|
||||
case 'triangle':
|
||||
drawTriangle(ctx, 0, 0, w, h);
|
||||
}
|
||||
|
||||
ctx.lineWidth = shapeModel.strokeWidth;
|
||||
ctx.strokeStyle = strokeColor;
|
||||
ctx.fillStyle = filled ? fillColor : 'transparent';
|
||||
|
||||
switch (shapeModel.strokeStyle) {
|
||||
case 'none':
|
||||
ctx.strokeStyle = 'transparent';
|
||||
break;
|
||||
case 'dash':
|
||||
ctx.setLineDash([12, 12]);
|
||||
break;
|
||||
}
|
||||
|
||||
if (shapeModel.shadow) {
|
||||
const { blur, offsetX, offsetY, color } = shapeModel.shadow;
|
||||
const scale = ctx.getTransform().a;
|
||||
|
||||
const enableShadowBlur = shapeModel.surface.doc
|
||||
.get(FeatureFlagService)
|
||||
.getFlag('enable_shape_shadow_blur');
|
||||
|
||||
// hard shadow, or soft shadow if `enable_shape_shadow_blur` is true
|
||||
// see comment of `shape.shadow` in `ShapeElementModel`
|
||||
if (blur === 0 || enableShadowBlur) {
|
||||
ctx.shadowBlur = blur * scale;
|
||||
ctx.shadowOffsetX = offsetX * scale;
|
||||
ctx.shadowOffsetY = offsetY * scale;
|
||||
}
|
||||
|
||||
ctx.shadowColor = renderer.getColorValue(color, undefined, true);
|
||||
}
|
||||
|
||||
ctx.stroke();
|
||||
ctx.fill();
|
||||
|
||||
if (shapeModel.shadow) {
|
||||
ctx.shadowBlur = 0;
|
||||
ctx.shadowOffsetX = 0;
|
||||
ctx.shadowOffsetY = 0;
|
||||
}
|
||||
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
function drawRect(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number,
|
||||
radius: number
|
||||
) {
|
||||
const r =
|
||||
radius < 1
|
||||
? Math.max(Math.min(width * radius, height * radius), 0)
|
||||
: radius;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x + r, y);
|
||||
ctx.lineTo(x + width - r, y);
|
||||
ctx.arcTo(x + width, y, x + width, y + r, r);
|
||||
ctx.lineTo(x + width, y + height - r);
|
||||
ctx.arcTo(x + width, y + height, x + width - r, y + height, r);
|
||||
ctx.lineTo(x + r, y + height);
|
||||
ctx.arcTo(x, y + height, x, y + height - r, r);
|
||||
ctx.lineTo(x, y + r);
|
||||
ctx.arcTo(x, y, x + r, y, r);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function drawDiamond(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(width / 2, y);
|
||||
ctx.lineTo(width, height / 2);
|
||||
ctx.lineTo(width / 2, height);
|
||||
ctx.lineTo(x, height / 2);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function drawEllipse(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
_x: number,
|
||||
_y: number,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
const cx = width / 2;
|
||||
const cy = height / 2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, width / 2, height / 2, 0, 0, 2 * Math.PI);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function drawTriangle(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: number,
|
||||
y: number,
|
||||
width: number,
|
||||
height: number
|
||||
) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(width / 2, y);
|
||||
ctx.lineTo(width, height);
|
||||
ctx.lineTo(x, height);
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
export function horizontalOffset(
|
||||
width: number,
|
||||
textAlign: TextAlign,
|
||||
horiPadding: number
|
||||
) {
|
||||
return textAlign === 'center'
|
||||
? width / 2
|
||||
: textAlign === 'right'
|
||||
? width - horiPadding
|
||||
: horiPadding;
|
||||
}
|
||||
|
||||
export function verticalOffset(
|
||||
lines: TextDelta[][],
|
||||
lineHeight: number,
|
||||
height: number,
|
||||
textVerticalAlign: TextVerticalAlign,
|
||||
verticalPadding: number
|
||||
) {
|
||||
return textVerticalAlign === 'center'
|
||||
? Math.max((height - lineHeight * lines.length) / 2, verticalPadding)
|
||||
: textVerticalAlign === 'top'
|
||||
? verticalPadding
|
||||
: height - lineHeight * lines.length - verticalPadding;
|
||||
}
|
||||
export function normalizeShapeBound(
|
||||
shape: ShapeElementModel,
|
||||
bound: Bound
|
||||
): Bound {
|
||||
if (!shape.text) return bound;
|
||||
|
||||
const [verticalPadding, horiPadding] = shape.padding;
|
||||
const yText = shape.text;
|
||||
const { fontFamily, fontSize, fontStyle, fontWeight } = shape;
|
||||
const lineHeight = getLineHeight(fontFamily, fontSize, fontWeight);
|
||||
const font = getFontString({
|
||||
fontStyle,
|
||||
fontWeight,
|
||||
fontSize,
|
||||
fontFamily,
|
||||
});
|
||||
const widestCharWidth =
|
||||
[...yText.toString()]
|
||||
.map(char => getTextWidth(char, font))
|
||||
.sort((a, b) => a - b)
|
||||
.pop() ?? getTextWidth('W', font);
|
||||
|
||||
if (bound.w < widestCharWidth + horiPadding * 2) {
|
||||
bound.w = widestCharWidth + horiPadding * 2;
|
||||
}
|
||||
const deltas: TextDelta[] = (yText.toDelta() as TextDelta[]).flatMap(
|
||||
delta => ({
|
||||
insert: wrapText(delta.insert, font, bound.w - horiPadding * 2),
|
||||
attributes: delta.attributes,
|
||||
})
|
||||
) as TextDelta[];
|
||||
const lines = deltaInsertsToChunks(deltas);
|
||||
|
||||
if (bound.h < lineHeight * lines.length + verticalPadding * 2) {
|
||||
bound.h = lineHeight * lines.length + verticalPadding * 2;
|
||||
}
|
||||
|
||||
return bound;
|
||||
}
|
||||
|
||||
export function fitContent(shape: ShapeElementModel) {
|
||||
const font = getFontString(shape);
|
||||
|
||||
if (!shape.text) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [verticalPadding, horiPadding] = shape.padding;
|
||||
const lines = deltaInsertsToChunks(
|
||||
wrapTextDeltas(shape.text, font, shape.maxWidth || Number.MAX_SAFE_INTEGER)
|
||||
);
|
||||
const { lineHeight, lineGap } = measureTextInDOM(
|
||||
shape.fontFamily,
|
||||
shape.fontSize,
|
||||
shape.fontWeight
|
||||
);
|
||||
let maxWidth = 0;
|
||||
let height = 0;
|
||||
|
||||
lines.forEach(line => {
|
||||
for (const delta of line) {
|
||||
const str = delta.insert;
|
||||
|
||||
maxWidth = Math.max(maxWidth, getLineWidth(str, font));
|
||||
}
|
||||
height += lineHeight + lineGap;
|
||||
});
|
||||
|
||||
height = Math.max(lineHeight + lineGap, height);
|
||||
|
||||
maxWidth += horiPadding * 2;
|
||||
height += verticalPadding * 2;
|
||||
|
||||
const newXYWH = `[${shape.x},${shape.y},${maxWidth},${height}]`;
|
||||
|
||||
if (shape.xywh !== newXYWH) {
|
||||
shape.xywh = newXYWH as SerializedXYWH;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
export * from './consts';
|
||||
export * from './draggable';
|
||||
export * from './element-renderer';
|
||||
export * from './overlay';
|
||||
export * from './shape-tool';
|
||||
export * from './text';
|
||||
|
||||
@@ -1,16 +1,10 @@
|
||||
import {
|
||||
EdgelessCRUDIdentifier,
|
||||
normalizeShapeBound,
|
||||
} from '@blocksuite/affine-block-surface';
|
||||
import { EdgelessCRUDIdentifier } from '@blocksuite/affine-block-surface';
|
||||
import {
|
||||
packColor,
|
||||
type PickColorEvent,
|
||||
} from '@blocksuite/affine-components/color-picker';
|
||||
import type { LineDetailType } from '@blocksuite/affine-components/edgeless-line-styles-panel';
|
||||
import {
|
||||
createMindmapLayoutActionMenu,
|
||||
createMindmapStyleActionMenu,
|
||||
} from '@blocksuite/affine-gfx-mindmap';
|
||||
import { createTextActions } from '@blocksuite/affine-gfx-text';
|
||||
import {
|
||||
type Color,
|
||||
DefaultTheme,
|
||||
@@ -35,7 +29,6 @@ import {
|
||||
} from '@blocksuite/affine-shared/services';
|
||||
import { getMostCommonValue } from '@blocksuite/affine-shared/utils';
|
||||
import {
|
||||
createTextActions,
|
||||
getRootBlock,
|
||||
LINE_STYLE_LIST,
|
||||
renderMenu,
|
||||
@@ -46,62 +39,13 @@ import { BlockFlavourIdentifier } from '@blocksuite/std';
|
||||
import { html } from 'lit';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
|
||||
import { normalizeShapeBound } from '../element-renderer';
|
||||
import type { ShapeToolOption } from '../shape-tool';
|
||||
import { mountShapeTextEditor } from '../text/edgeless-shape-text-editor';
|
||||
import { ShapeComponentConfig } from './shape-menu-config';
|
||||
|
||||
export const shapeToolbarConfig = {
|
||||
actions: [
|
||||
{
|
||||
id: 'a.mindmap-style',
|
||||
when(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||
return models.some(hasGrouped);
|
||||
},
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
let mindmaps = models
|
||||
.map(model => model.group)
|
||||
.filter(model => ctx.matchModel(model, MindmapElementModel));
|
||||
if (!mindmaps.length) return null;
|
||||
|
||||
// Not displayed when there is both a normal shape and a mindmap shape.
|
||||
if (models.length !== mindmaps.length) return null;
|
||||
|
||||
mindmaps = Array.from(new Set(mindmaps));
|
||||
|
||||
return createMindmapStyleActionMenu(ctx, mindmaps);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'b.mindmap-layout',
|
||||
when(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||
return models.some(hasGrouped);
|
||||
},
|
||||
content(ctx) {
|
||||
const models = ctx.getSurfaceModelsByType(ShapeElementModel);
|
||||
if (!models.length) return null;
|
||||
|
||||
let mindmaps = models
|
||||
.map(model => model.group)
|
||||
.filter(model => ctx.matchModel(model, MindmapElementModel));
|
||||
if (!mindmaps.length) return null;
|
||||
|
||||
// Not displayed when there is both a normal shape and a mindmap shape.
|
||||
if (models.length !== mindmaps.length) return null;
|
||||
|
||||
mindmaps = Array.from(new Set(mindmaps));
|
||||
|
||||
// It's a sub node.
|
||||
if (models.length === 1 && mindmaps[0].tree.element !== models[0])
|
||||
return null;
|
||||
|
||||
return createMindmapLayoutActionMenu(ctx, mindmaps);
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'c.switch-type',
|
||||
when(ctx) {
|
||||
@@ -386,7 +330,7 @@ function getTextColor(fillColor: Color, isNotTransparent = false) {
|
||||
return DefaultTheme.shapeTextColor;
|
||||
}
|
||||
|
||||
function hasGrouped(model: ShapeElementModel) {
|
||||
export function hasGrouped(model: ShapeElementModel) {
|
||||
return model.group instanceof MindmapElementModel;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user