mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 02:00:49 +08:00
refactor: extract common builder
This commit is contained in:
@@ -5,7 +5,7 @@ import {
|
||||
DefaultTheme,
|
||||
type PointStyle,
|
||||
} from '@blocksuite/affine-model';
|
||||
import { PointLocation } from '@blocksuite/global/gfx';
|
||||
import { PointLocation, SVGPathBuilder } from '@blocksuite/global/gfx';
|
||||
|
||||
import { isConnectorWithLabel } from '../../connector-manager.js';
|
||||
import { DEFAULT_ARROW_SIZE } from '../utils.js';
|
||||
@@ -17,60 +17,6 @@ interface PathBounds {
|
||||
maxY: number;
|
||||
}
|
||||
|
||||
interface PathCommand {
|
||||
command: string;
|
||||
coordinates: number[];
|
||||
}
|
||||
|
||||
class SVGPathBuilder {
|
||||
private commands: PathCommand[] = [];
|
||||
|
||||
moveTo(x: number, y: number): this {
|
||||
this.commands.push({
|
||||
command: 'M',
|
||||
coordinates: [x, y],
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
lineTo(x: number, y: number): this {
|
||||
this.commands.push({
|
||||
command: 'L',
|
||||
coordinates: [x, y],
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
build(): string {
|
||||
const pathSegments = this.commands.map(cmd => {
|
||||
const coords = cmd.coordinates.join(' ');
|
||||
return `${cmd.command} ${coords}`;
|
||||
});
|
||||
|
||||
return pathSegments.join(' ');
|
||||
}
|
||||
|
||||
clear(): this {
|
||||
this.commands = [];
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
function calculatePathBounds(path: PointLocation[]): PathBounds {
|
||||
if (path.length === 0) {
|
||||
return { minX: 0, minY: 0, maxX: 0, maxY: 0 };
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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 { manageClassNames, setStyles } from './utils';
|
||||
|
||||
@@ -122,25 +123,22 @@ export const shapeDomRenderer = (
|
||||
element.style.backgroundColor = 'transparent'; // Host element is transparent
|
||||
|
||||
const strokeW = model.strokeWidth;
|
||||
const halfStroke = strokeW / 2; // Calculate half stroke width for point adjustment
|
||||
|
||||
let svgPoints = '';
|
||||
if (model.shapeType === 'diamond') {
|
||||
// Adjusted points for diamond
|
||||
svgPoints = [
|
||||
`${unscaledWidth / 2},${halfStroke}`,
|
||||
`${unscaledWidth - halfStroke},${unscaledHeight / 2}`,
|
||||
`${unscaledWidth / 2},${unscaledHeight - halfStroke}`,
|
||||
`${halfStroke},${unscaledHeight / 2}`,
|
||||
].join(' ');
|
||||
// Generate diamond points using shared utility
|
||||
svgPoints = SVGShapeBuilder.diamond(
|
||||
unscaledWidth,
|
||||
unscaledHeight,
|
||||
strokeW
|
||||
);
|
||||
} else {
|
||||
// triangle
|
||||
// Adjusted points for triangle
|
||||
svgPoints = [
|
||||
`${unscaledWidth / 2},${halfStroke}`,
|
||||
`${unscaledWidth - halfStroke},${unscaledHeight - halfStroke}`,
|
||||
`${halfStroke},${unscaledHeight - halfStroke}`,
|
||||
].join(' ');
|
||||
// triangle - generate triangle points using shared utility
|
||||
svgPoints = SVGShapeBuilder.triangle(
|
||||
unscaledWidth,
|
||||
unscaledHeight,
|
||||
strokeW
|
||||
);
|
||||
}
|
||||
|
||||
// Determine if stroke should be visible and its color
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
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,4 +4,5 @@ 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';
|
||||
|
||||
@@ -0,0 +1,160 @@
|
||||
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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user