mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 02:00:49 +08:00
refactor(editor): migrate to svgjs to simplify dom renderer
This commit is contained in:
@@ -23,6 +23,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@svgdotjs/svg.js": "^3.2.4",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { DomRenderer } from '@blocksuite/affine-block-surface';
|
||||
import type { BrushElementModel } from '@blocksuite/affine-model';
|
||||
import { DefaultTheme } from '@blocksuite/affine-model';
|
||||
import { SVG } from '@svgdotjs/svg.js';
|
||||
|
||||
/**
|
||||
* Renders a BrushElementModel to a given HTMLElement using DOM properties.
|
||||
@@ -29,22 +30,19 @@ export const brushDomRenderer = (
|
||||
// Clear any existing content
|
||||
element.replaceChildren();
|
||||
|
||||
// Create SVG element to render the brush stroke
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
const svg = document.createElementNS(SVG_NS, 'svg');
|
||||
svg.setAttribute('width', '100%');
|
||||
svg.setAttribute('height', '100%');
|
||||
svg.setAttribute('viewBox', `0 0 ${unscaledWidth} ${unscaledHeight}`);
|
||||
svg.setAttribute('preserveAspectRatio', 'none');
|
||||
// Create SVG element using svg.js to render the brush stroke
|
||||
const svg = SVG().addTo(element).size('100%', '100%');
|
||||
svg.attr({
|
||||
viewBox: `0 0 ${unscaledWidth} ${unscaledHeight}`,
|
||||
preserveAspectRatio: 'none',
|
||||
});
|
||||
|
||||
// Create path element for the brush stroke
|
||||
const path = document.createElementNS(SVG_NS, 'path');
|
||||
path.setAttribute('d', model.commands);
|
||||
path.setAttribute('fill', color);
|
||||
path.setAttribute('stroke', 'none');
|
||||
|
||||
svg.append(path);
|
||||
element.append(svg);
|
||||
const path = svg.path(model.commands);
|
||||
path.attr({
|
||||
fill: color,
|
||||
stroke: 'none',
|
||||
});
|
||||
|
||||
// Apply rotation if needed
|
||||
if (model.rotate) {
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@svgdotjs/svg.js": "^3.2.4",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
|
||||
@@ -5,7 +5,8 @@ import {
|
||||
DefaultTheme,
|
||||
type PointStyle,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { PointLocation, SVGPathBuilder } from '@blocksuite/global/gfx';
|
||||
import { PointLocation } from '@blocksuite/global/gfx';
|
||||
import { SVG } from '@svgdotjs/svg.js';
|
||||
|
||||
import { isConnectorWithLabel } from '../../connector-manager.js';
|
||||
import { DEFAULT_ARROW_SIZE } from '../utils.js';
|
||||
@@ -43,111 +44,71 @@ function createConnectorPath(
|
||||
): string {
|
||||
if (points.length < 2) return '';
|
||||
|
||||
const pathBuilder = new SVGPathBuilder();
|
||||
pathBuilder.moveTo(points[0][0], points[0][1]);
|
||||
let pathData = `M ${points[0][0]} ${points[0][1]}`;
|
||||
|
||||
if (mode === ConnectorMode.Curve) {
|
||||
// Use bezier curves
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
const prev = points[i - 1];
|
||||
const curr = points[i];
|
||||
pathBuilder.curveTo(
|
||||
prev.absOut[0],
|
||||
prev.absOut[1],
|
||||
curr.absIn[0],
|
||||
curr.absIn[1],
|
||||
curr[0],
|
||||
curr[1]
|
||||
);
|
||||
pathData += ` C ${prev.absOut[0]} ${prev.absOut[1]} ${curr.absIn[0]} ${curr.absIn[1]} ${curr[0]} ${curr[1]}`;
|
||||
}
|
||||
} else {
|
||||
// Use straight lines
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
pathBuilder.lineTo(points[i][0], points[i][1]);
|
||||
pathData += ` L ${points[i][0]} ${points[i][1]}`;
|
||||
}
|
||||
}
|
||||
|
||||
return pathBuilder.build();
|
||||
return pathData;
|
||||
}
|
||||
|
||||
function createArrowMarker(
|
||||
svg: any,
|
||||
id: string,
|
||||
style: PointStyle,
|
||||
color: string,
|
||||
strokeWidth: number,
|
||||
isStart: boolean = false
|
||||
): SVGMarkerElement {
|
||||
const marker = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'marker'
|
||||
);
|
||||
): void {
|
||||
const size = DEFAULT_ARROW_SIZE * (strokeWidth / 2);
|
||||
const defs = svg.defs();
|
||||
|
||||
marker.id = id;
|
||||
marker.setAttribute('viewBox', '0 0 20 20');
|
||||
marker.setAttribute('refX', isStart ? '20' : '0');
|
||||
marker.setAttribute('refY', '10');
|
||||
marker.setAttribute('markerWidth', String(size));
|
||||
marker.setAttribute('markerHeight', String(size));
|
||||
marker.setAttribute('orient', 'auto');
|
||||
marker.setAttribute('markerUnits', 'strokeWidth');
|
||||
const marker = defs.marker(size, size, function (add: any) {
|
||||
switch (style) {
|
||||
case 'Arrow': {
|
||||
add
|
||||
.path(isStart ? 'M 20 5 L 10 10 L 20 15 Z' : 'M 0 5 L 10 10 L 0 15 Z')
|
||||
.fill(color)
|
||||
.stroke(color);
|
||||
break;
|
||||
}
|
||||
case 'Triangle': {
|
||||
add
|
||||
.path(isStart ? 'M 20 7 L 12 10 L 20 13 Z' : 'M 0 7 L 8 10 L 0 13 Z')
|
||||
.fill(color)
|
||||
.stroke(color);
|
||||
break;
|
||||
}
|
||||
case 'Circle': {
|
||||
add.circle(8).center(10, 10).fill(color).stroke(color);
|
||||
break;
|
||||
}
|
||||
case 'Diamond': {
|
||||
add.path('M 10 6 L 14 10 L 10 14 L 6 10 Z').fill(color).stroke(color);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
switch (style) {
|
||||
case 'Arrow': {
|
||||
const path = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'path'
|
||||
);
|
||||
path.setAttribute(
|
||||
'd',
|
||||
isStart ? 'M 20 5 L 10 10 L 20 15 Z' : 'M 0 5 L 10 10 L 0 15 Z'
|
||||
);
|
||||
path.setAttribute('fill', color);
|
||||
path.setAttribute('stroke', color);
|
||||
marker.append(path);
|
||||
break;
|
||||
}
|
||||
case 'Triangle': {
|
||||
const path = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'path'
|
||||
);
|
||||
path.setAttribute(
|
||||
'd',
|
||||
isStart ? 'M 20 7 L 12 10 L 20 13 Z' : 'M 0 7 L 8 10 L 0 13 Z'
|
||||
);
|
||||
path.setAttribute('fill', color);
|
||||
path.setAttribute('stroke', color);
|
||||
marker.append(path);
|
||||
break;
|
||||
}
|
||||
case 'Circle': {
|
||||
const circle = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'circle'
|
||||
);
|
||||
circle.setAttribute('cx', '10');
|
||||
circle.setAttribute('cy', '10');
|
||||
circle.setAttribute('r', '4');
|
||||
circle.setAttribute('fill', color);
|
||||
circle.setAttribute('stroke', color);
|
||||
marker.append(circle);
|
||||
break;
|
||||
}
|
||||
case 'Diamond': {
|
||||
const path = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'path'
|
||||
);
|
||||
path.setAttribute('d', 'M 10 6 L 14 10 L 10 14 L 6 10 Z');
|
||||
path.setAttribute('fill', color);
|
||||
path.setAttribute('stroke', color);
|
||||
marker.append(path);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return marker;
|
||||
marker.id(id);
|
||||
marker.attr({
|
||||
viewBox: '0 0 20 20',
|
||||
refX: isStart ? '20' : '0',
|
||||
refY: '10',
|
||||
orient: 'auto',
|
||||
markerUnits: 'strokeWidth',
|
||||
});
|
||||
}
|
||||
|
||||
function renderConnectorLabel(
|
||||
@@ -253,20 +214,12 @@ export const connectorDomRenderer = (
|
||||
const offsetX = pathBounds.minX - padding;
|
||||
const offsetY = pathBounds.minY - padding;
|
||||
|
||||
// Create SVG element
|
||||
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
svg.style.position = 'absolute';
|
||||
svg.style.left = `${offsetX * zoom}px`;
|
||||
svg.style.top = `${offsetY * zoom}px`;
|
||||
svg.style.width = `${svgWidth}px`;
|
||||
svg.style.height = `${svgHeight}px`;
|
||||
svg.style.overflow = 'visible';
|
||||
svg.style.pointerEvents = 'none';
|
||||
svg.setAttribute('viewBox', `0 0 ${svgWidth / zoom} ${svgHeight / zoom}`);
|
||||
|
||||
// Create defs for markers
|
||||
const defs = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
|
||||
svg.append(defs);
|
||||
// Create SVG using svg.js
|
||||
const svg = SVG().addTo(element).size(svgWidth, svgHeight);
|
||||
svg.attr({
|
||||
style: `position: absolute; left: ${offsetX * zoom}px; top: ${offsetY * zoom}px; overflow: visible; pointer-events: none;`,
|
||||
viewBox: `0 0 ${svgWidth / zoom} ${svgHeight / zoom}`,
|
||||
});
|
||||
|
||||
const strokeColor = renderer.getColorValue(
|
||||
stroke,
|
||||
@@ -280,34 +233,28 @@ export const connectorDomRenderer = (
|
||||
|
||||
if (frontEndpointStyle !== 'None') {
|
||||
startMarkerId = `start-marker-${model.id}`;
|
||||
const startMarker = createArrowMarker(
|
||||
createArrowMarker(
|
||||
svg,
|
||||
startMarkerId,
|
||||
frontEndpointStyle,
|
||||
strokeColor,
|
||||
strokeWidth,
|
||||
true
|
||||
);
|
||||
defs.append(startMarker);
|
||||
}
|
||||
|
||||
if (rearEndpointStyle !== 'None') {
|
||||
endMarkerId = `end-marker-${model.id}`;
|
||||
const endMarker = createArrowMarker(
|
||||
createArrowMarker(
|
||||
svg,
|
||||
endMarkerId,
|
||||
rearEndpointStyle,
|
||||
strokeColor,
|
||||
strokeWidth,
|
||||
false
|
||||
);
|
||||
defs.append(endMarker);
|
||||
}
|
||||
|
||||
// Create path element
|
||||
const pathElement = document.createElementNS(
|
||||
'http://www.w3.org/2000/svg',
|
||||
'path'
|
||||
);
|
||||
|
||||
// Adjust points relative to the SVG coordinate system
|
||||
const adjustedPoints = points.map(point => {
|
||||
const adjustedPoint = new PointLocation([
|
||||
@@ -329,30 +276,31 @@ export const connectorDomRenderer = (
|
||||
return adjustedPoint;
|
||||
});
|
||||
|
||||
// Create path element using svg.js
|
||||
const pathData = createConnectorPath(adjustedPoints, mode);
|
||||
pathElement.setAttribute('d', pathData);
|
||||
pathElement.setAttribute('stroke', strokeColor);
|
||||
pathElement.setAttribute('stroke-width', String(strokeWidth));
|
||||
pathElement.setAttribute('fill', 'none');
|
||||
pathElement.setAttribute('stroke-linecap', 'round');
|
||||
pathElement.setAttribute('stroke-linejoin', 'round');
|
||||
const pathElement = svg.path(pathData);
|
||||
|
||||
pathElement.attr({
|
||||
stroke: strokeColor,
|
||||
'stroke-width': strokeWidth,
|
||||
fill: 'none',
|
||||
'stroke-linecap': 'round',
|
||||
'stroke-linejoin': 'round',
|
||||
});
|
||||
|
||||
// Apply stroke style
|
||||
if (strokeStyle === 'dash') {
|
||||
pathElement.setAttribute('stroke-dasharray', '12,12');
|
||||
pathElement.attr('stroke-dasharray', '12,12');
|
||||
}
|
||||
|
||||
// Apply markers
|
||||
if (startMarkerId) {
|
||||
pathElement.setAttribute('marker-start', `url(#${startMarkerId})`);
|
||||
pathElement.attr('marker-start', `url(#${startMarkerId})`);
|
||||
}
|
||||
if (endMarkerId) {
|
||||
pathElement.setAttribute('marker-end', `url(#${endMarkerId})`);
|
||||
pathElement.attr('marker-end', `url(#${endMarkerId})`);
|
||||
}
|
||||
|
||||
svg.append(pathElement);
|
||||
element.append(svg);
|
||||
|
||||
// Set element size and position
|
||||
element.style.width = `${model.w * zoom}px`;
|
||||
element.style.height = `${model.h * zoom}px`;
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"@blocksuite/store": "workspace:*",
|
||||
"@lit/context": "^1.1.2",
|
||||
"@preact/signals-core": "^1.8.0",
|
||||
"@svgdotjs/svg.js": "^3.2.4",
|
||||
"@toeverything/theme": "^1.1.15",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
"lit": "^3.2.0",
|
||||
|
||||
@@ -1,10 +1,37 @@
|
||||
import type { DomRenderer } from '@blocksuite/affine-block-surface';
|
||||
import type { ShapeElementModel } from '@blocksuite/affine-model';
|
||||
import { DefaultTheme } from '@blocksuite/affine-model';
|
||||
import { SVGShapeBuilder } from '@blocksuite/global/gfx';
|
||||
import { SVG } from '@svgdotjs/svg.js';
|
||||
|
||||
import { manageClassNames, setStyles } from './utils';
|
||||
|
||||
function createDiamondPoints(
|
||||
width: number,
|
||||
height: number,
|
||||
strokeWidth: number = 0
|
||||
): string {
|
||||
const halfStroke = strokeWidth / 2;
|
||||
return [
|
||||
`${width / 2},${halfStroke}`,
|
||||
`${width - halfStroke},${height / 2}`,
|
||||
`${width / 2},${height - halfStroke}`,
|
||||
`${halfStroke},${height / 2}`,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function createTrianglePoints(
|
||||
width: number,
|
||||
height: number,
|
||||
strokeWidth: number = 0
|
||||
): string {
|
||||
const halfStroke = strokeWidth / 2;
|
||||
return [
|
||||
`${width / 2},${halfStroke}`,
|
||||
`${width - halfStroke},${height - halfStroke}`,
|
||||
`${halfStroke},${height - halfStroke}`,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
function applyShapeSpecificStyles(
|
||||
model: ShapeElementModel,
|
||||
element: HTMLElement,
|
||||
@@ -126,19 +153,11 @@ export const shapeDomRenderer = (
|
||||
|
||||
let svgPoints = '';
|
||||
if (model.shapeType === 'diamond') {
|
||||
// Generate diamond points using shared utility
|
||||
svgPoints = SVGShapeBuilder.diamond(
|
||||
unscaledWidth,
|
||||
unscaledHeight,
|
||||
strokeW
|
||||
);
|
||||
// Generate diamond points directly
|
||||
svgPoints = createDiamondPoints(unscaledWidth, unscaledHeight, strokeW);
|
||||
} else {
|
||||
// triangle - generate triangle points using shared utility
|
||||
svgPoints = SVGShapeBuilder.triangle(
|
||||
unscaledWidth,
|
||||
unscaledHeight,
|
||||
strokeW
|
||||
);
|
||||
// Generate triangle points directly
|
||||
svgPoints = createTrianglePoints(unscaledWidth, unscaledHeight, strokeW);
|
||||
}
|
||||
|
||||
// Determine if stroke should be visible and its color
|
||||
@@ -152,26 +171,26 @@ export const shapeDomRenderer = (
|
||||
// Determine fill color
|
||||
const finalFillColor = model.filled ? fillColor : 'transparent';
|
||||
|
||||
// Build SVG safely with DOM-API
|
||||
const SVG_NS = 'http://www.w3.org/2000/svg';
|
||||
const svg = document.createElementNS(SVG_NS, 'svg');
|
||||
svg.setAttribute('width', '100%');
|
||||
svg.setAttribute('height', '100%');
|
||||
svg.setAttribute('viewBox', `0 0 ${unscaledWidth} ${unscaledHeight}`);
|
||||
svg.setAttribute('preserveAspectRatio', 'none');
|
||||
// Build SVG using svg.js
|
||||
const svg = SVG().addTo(element).size('100%', '100%');
|
||||
svg.attr({
|
||||
viewBox: `0 0 ${unscaledWidth} ${unscaledHeight}`,
|
||||
preserveAspectRatio: 'none',
|
||||
});
|
||||
|
||||
const polygon = svg.polygon(svgPoints);
|
||||
polygon.attr({
|
||||
fill: finalFillColor,
|
||||
stroke: finalStrokeColor,
|
||||
'stroke-width': strokeW,
|
||||
});
|
||||
|
||||
const polygon = document.createElementNS(SVG_NS, 'polygon');
|
||||
polygon.setAttribute('points', svgPoints);
|
||||
polygon.setAttribute('fill', finalFillColor);
|
||||
polygon.setAttribute('stroke', finalStrokeColor);
|
||||
polygon.setAttribute('stroke-width', String(strokeW));
|
||||
if (finalStrokeDasharray !== 'none') {
|
||||
polygon.setAttribute('stroke-dasharray', finalStrokeDasharray);
|
||||
polygon.attr('stroke-dasharray', finalStrokeDasharray);
|
||||
}
|
||||
svg.append(polygon);
|
||||
|
||||
// Replace existing children to avoid memory leaks
|
||||
element.replaceChildren(svg);
|
||||
element.replaceChildren(svg.node);
|
||||
} else {
|
||||
// Standard rendering for other shapes (e.g., rect, ellipse)
|
||||
// innerHTML was already cleared by applyShapeSpecificStyles if necessary
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
|
||||
import { SVGPathBuilder, SVGShapeBuilder } from '../gfx/svg-path.js';
|
||||
|
||||
describe('SVGPathBuilder', () => {
|
||||
test('should build a simple path', () => {
|
||||
const pathBuilder = new SVGPathBuilder();
|
||||
const result = pathBuilder.moveTo(10, 20).lineTo(30, 40).build();
|
||||
|
||||
expect(result).toBe('M 10 20 L 30 40');
|
||||
});
|
||||
|
||||
test('should build a path with curves', () => {
|
||||
const pathBuilder = new SVGPathBuilder();
|
||||
const result = pathBuilder
|
||||
.moveTo(0, 0)
|
||||
.curveTo(10, 0, 10, 10, 20, 10)
|
||||
.build();
|
||||
|
||||
expect(result).toBe('M 0 0 C 10 0 10 10 20 10');
|
||||
});
|
||||
|
||||
test('should build a closed path', () => {
|
||||
const pathBuilder = new SVGPathBuilder();
|
||||
const result = pathBuilder
|
||||
.moveTo(0, 0)
|
||||
.lineTo(10, 0)
|
||||
.lineTo(5, 10)
|
||||
.closePath()
|
||||
.build();
|
||||
|
||||
expect(result).toBe('M 0 0 L 10 0 L 5 10 Z');
|
||||
});
|
||||
|
||||
test('should clear commands', () => {
|
||||
const pathBuilder = new SVGPathBuilder();
|
||||
pathBuilder.moveTo(10, 20).lineTo(30, 40);
|
||||
pathBuilder.clear();
|
||||
const result = pathBuilder.moveTo(0, 0).build();
|
||||
|
||||
expect(result).toBe('M 0 0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SVGShapeBuilder', () => {
|
||||
test('should generate diamond polygon points', () => {
|
||||
const result = SVGShapeBuilder.diamond(100, 80, 2);
|
||||
expect(result).toBe('50,1 99,40 50,79 1,40');
|
||||
});
|
||||
|
||||
test('should generate triangle polygon points', () => {
|
||||
const result = SVGShapeBuilder.triangle(100, 80, 2);
|
||||
expect(result).toBe('50,1 99,79 1,79');
|
||||
});
|
||||
|
||||
test('should generate diamond path', () => {
|
||||
const result = SVGShapeBuilder.diamondPath(100, 80, 2);
|
||||
expect(result).toBe('M 50 1 L 99 40 L 50 79 L 1 40 Z');
|
||||
});
|
||||
|
||||
test('should generate triangle path', () => {
|
||||
const result = SVGShapeBuilder.trianglePath(100, 80, 2);
|
||||
expect(result).toBe('M 50 1 L 99 79 L 1 79 Z');
|
||||
});
|
||||
|
||||
test('should handle zero stroke width', () => {
|
||||
const diamondResult = SVGShapeBuilder.diamond(100, 80, 0);
|
||||
expect(diamondResult).toBe('50,0 100,40 50,80 0,40');
|
||||
|
||||
const triangleResult = SVGShapeBuilder.triangle(100, 80, 0);
|
||||
expect(triangleResult).toBe('50,0 100,80 0,80');
|
||||
});
|
||||
});
|
||||
@@ -4,5 +4,4 @@ export * from './math.js';
|
||||
export * from './model/index.js';
|
||||
export * from './perfect-freehand/index.js';
|
||||
export * from './polyline.js';
|
||||
export * from './svg-path.js';
|
||||
export * from './xywh.js';
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
interface PathCommand {
|
||||
command: string;
|
||||
coordinates: number[];
|
||||
}
|
||||
|
||||
/**
|
||||
* A utility class for building SVG path strings using command-based API.
|
||||
* Supports moveTo, lineTo, curveTo operations and can build complete path strings.
|
||||
*/
|
||||
export class SVGPathBuilder {
|
||||
private commands: PathCommand[] = [];
|
||||
|
||||
/**
|
||||
* Move to a specific point without drawing
|
||||
*/
|
||||
moveTo(x: number, y: number): this {
|
||||
this.commands.push({
|
||||
command: 'M',
|
||||
coordinates: [x, y],
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a line to a specific point
|
||||
*/
|
||||
lineTo(x: number, y: number): this {
|
||||
this.commands.push({
|
||||
command: 'L',
|
||||
coordinates: [x, y],
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a cubic Bézier curve
|
||||
*/
|
||||
curveTo(
|
||||
cp1x: number,
|
||||
cp1y: number,
|
||||
cp2x: number,
|
||||
cp2y: number,
|
||||
x: number,
|
||||
y: number
|
||||
): this {
|
||||
this.commands.push({
|
||||
command: 'C',
|
||||
coordinates: [cp1x, cp1y, cp2x, cp2y, x, y],
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the current path
|
||||
*/
|
||||
closePath(): this {
|
||||
this.commands.push({
|
||||
command: 'Z',
|
||||
coordinates: [],
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the complete SVG path string
|
||||
*/
|
||||
build(): string {
|
||||
const pathSegments = this.commands.map(cmd => {
|
||||
const coords = cmd.coordinates.join(' ');
|
||||
return coords ? `${cmd.command} ${coords}` : cmd.command;
|
||||
});
|
||||
|
||||
return pathSegments.join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all commands and reset the builder
|
||||
*/
|
||||
clear(): this {
|
||||
this.commands = [];
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SVG polygon points string for common shapes
|
||||
*/
|
||||
export class SVGShapeBuilder {
|
||||
/**
|
||||
* Generate diamond (rhombus) polygon points
|
||||
*/
|
||||
static diamond(
|
||||
width: number,
|
||||
height: number,
|
||||
strokeWidth: number = 0
|
||||
): string {
|
||||
const halfStroke = strokeWidth / 2;
|
||||
return [
|
||||
`${width / 2},${halfStroke}`,
|
||||
`${width - halfStroke},${height / 2}`,
|
||||
`${width / 2},${height - halfStroke}`,
|
||||
`${halfStroke},${height / 2}`,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate triangle polygon points
|
||||
*/
|
||||
static triangle(
|
||||
width: number,
|
||||
height: number,
|
||||
strokeWidth: number = 0
|
||||
): string {
|
||||
const halfStroke = strokeWidth / 2;
|
||||
return [
|
||||
`${width / 2},${halfStroke}`,
|
||||
`${width - halfStroke},${height - halfStroke}`,
|
||||
`${halfStroke},${height - halfStroke}`,
|
||||
].join(' ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate diamond path using SVGPathBuilder
|
||||
*/
|
||||
static diamondPath(
|
||||
width: number,
|
||||
height: number,
|
||||
strokeWidth: number = 0
|
||||
): string {
|
||||
const halfStroke = strokeWidth / 2;
|
||||
const pathBuilder = new SVGPathBuilder();
|
||||
|
||||
return pathBuilder
|
||||
.moveTo(width / 2, halfStroke)
|
||||
.lineTo(width - halfStroke, height / 2)
|
||||
.lineTo(width / 2, height - halfStroke)
|
||||
.lineTo(halfStroke, height / 2)
|
||||
.closePath()
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate triangle path using SVGPathBuilder
|
||||
*/
|
||||
static trianglePath(
|
||||
width: number,
|
||||
height: number,
|
||||
strokeWidth: number = 0
|
||||
): string {
|
||||
const halfStroke = strokeWidth / 2;
|
||||
const pathBuilder = new SVGPathBuilder();
|
||||
|
||||
return pathBuilder
|
||||
.moveTo(width / 2, halfStroke)
|
||||
.lineTo(width - halfStroke, height - halfStroke)
|
||||
.lineTo(halfStroke, height - halfStroke)
|
||||
.closePath()
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -822,7 +822,7 @@ export async function updateExistedBrushElementSize(
|
||||
) {
|
||||
// get the nth brush size button
|
||||
const btn = page.locator(
|
||||
`edgeless-line-width-panel .point-button:nth-child(${nthSizeButton})`
|
||||
`editor-toolbar edgeless-line-width-panel .point-button:nth-child(${nthSizeButton})`
|
||||
);
|
||||
|
||||
await btn.click();
|
||||
|
||||
Reference in New Issue
Block a user