Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/whiteboard/shape-paths.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { WNode } from './types.js';
import { BORDER_WIDTH_MAP, r } from './utils.js';
import { BORDER_WIDTH_MAP, r, withoutStrokeAttrs } from './utils.js';

// ─── Shape Path Registry ────────────────────────────────────────────────────

Expand Down Expand Up @@ -87,9 +87,12 @@ registerShapePath(['trapezoid', 'flow_chart_trapezoid'], (w, h, sa) => {

registerShapePath(['cylinder', 'flow_chart_cylinder', 'data_base'], (w, h, sa, n) => {
const ry = h * 0.1;
// 圆柱体侧面用 line 描边,中间矩形不能再继承 sa 里的 stroke,
// 否则会出现 stroke="..." stroke="none" 的非法重复属性。
const bodySa = withoutStrokeAttrs(sa);
return [
`<ellipse cx="${w / 2}" cy="${ry}" rx="${w / 2}" ry="${ry}" ${sa}/>`,
`<rect x="0" y="${ry}" width="${w}" height="${h - 2 * ry}" ${sa} stroke="none"/>`,
`<rect x="0" y="${ry}" width="${w}" height="${h - 2 * ry}" ${bodySa} stroke="none"/>`,
`<line x1="0" y1="${ry}" x2="0" y2="${h - ry}" stroke="${n.style?.border_color ?? '#333'}" stroke-width="${BORDER_WIDTH_MAP[n.style?.border_width ?? 'narrow'] ?? 1}"/>`,
`<line x1="${w}" y1="${ry}" x2="${w}" y2="${h - ry}" stroke="${n.style?.border_color ?? '#333'}" stroke-width="${BORDER_WIDTH_MAP[n.style?.border_width ?? 'narrow'] ?? 1}"/>`,
`<ellipse cx="${w / 2}" cy="${h - ry}" rx="${w / 2}" ry="${ry}" ${sa}/>`,
Expand Down
8 changes: 8 additions & 0 deletions src/whiteboard/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,14 @@ export function styleAttrs (s?: WStyle): string {
return parts.join(' ');
}

/** 从 styleAttrs 结果中剥离 stroke 相关属性,便于再单独指定 stroke */
export function withoutStrokeAttrs (sa: string): string {
return sa
.replace(/\bstroke(?:-width|-opacity|-dasharray)?="[^"]*"/g, '')
.replace(/\s+/g, ' ')
.trim();
}

export function transformAttr (n: WNode): string {
const parts: string[] = [];
if (n.x != null || n.y != null) parts.push(`translate(${r(n.x ?? 0)},${r(n.y ?? 0)})`);
Expand Down
12 changes: 12 additions & 0 deletions tests/whiteboard/shape-paths.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,18 @@ describe('shape-paths: composite shapes with multiple elements', () => {
expect(out).toContain('stroke="#444"');
});

it('cylinder body rect does not redefine stroke attribute', () => {
const fn = getShapePath('cylinder')!;
const sa = 'fill="#dff5e5" stroke="#000000" stroke-width="2"';
const out = fn(100, 80, sa, blankNode({ style: { border_color: '#000000' } as any }));
const body = out.split('\n').find((line) => line.includes('<rect'))!;
// 每个属性名只能出现一次,避免浏览器报 Attribute stroke redefined
expect(body).toMatch(/stroke="none"/);
expect(body.match(/\bstroke="/g)?.length).toBe(1);
expect(body).not.toMatch(/stroke-width=/);
expect(body).toContain('fill="#dff5e5"');
});

it('cylinder uses default stroke when border_color missing', () => {
const fn = getShapePath('cylinder')!;
const out = fn(100, 80, SA, blankNode());
Expand Down
17 changes: 16 additions & 1 deletion tests/whiteboard/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import {
r, esc, styleAttrs, transformAttr, walkNodes, filterNodes, gWrap, nodeDataAttrs,
r, esc, styleAttrs, withoutStrokeAttrs, transformAttr, walkNodes, filterNodes, gWrap, nodeDataAttrs,
BORDER_WIDTH_MAP, DASH_ARRAY_MAP,
} from '../../src/whiteboard/utils.js';

Expand Down Expand Up @@ -78,6 +78,21 @@ describe('styleAttrs', () => {
});
});

describe('withoutStrokeAttrs', () => {
it('strips stroke and related attributes while keeping fill', () => {
expect(withoutStrokeAttrs('fill="#eee" stroke="#333"')).toBe('fill="#eee"');
expect(
withoutStrokeAttrs('fill="#dff5e5" stroke="#000" stroke-width="2" stroke-dasharray="8,4"'),
).toBe('fill="#dff5e5"');
});

it('keeps fill-opacity', () => {
expect(withoutStrokeAttrs('fill="#f00" fill-opacity="0.5" stroke="#333"')).toBe(
'fill="#f00" fill-opacity="0.5"',
);
});
});

// ─── transformAttr ──────────────────────────────────────────────────────────

describe('transformAttr', () => {
Expand Down