Skip to content

Commit 1a3662e

Browse files
authored
feat(structure): introduce sequence-interaction structure with hierarchical layout (#204)
* feat(structure): introduce sequence-interaction structure * fix(structure): fix sequence-interaction node icon rendering * feat(structure): enhance interactivity and layout for sequence-interaction * refactor: address CR feedback by refactoring arrow logic and removing unused code * refacotr: optimize edge path * refactor: extract geometry utilities and optimize arrow rendering * feat: support custom vertical step ordering in sequence interactions * test: add utils/geometry test * feat: implement edge masking and improve relation syntax parsing * fix: regenerate SSR test golden files to align with the updated text measurement logic * fix: remove implicit PI offset in getTangentAngle and handle it in caller * fix: resolve global ID conflicts in SVG * feat: improve parsing of double-headed arrows in relations * feat: add fallback support for items with relations and nested children. * docs: update sequence-interaction structure description in skills * test: update parse-syntax test * docs: 精简下模板 * fix: fix background leakage at rounded corners by adjusting lifeline overlays
1 parent a46224c commit 1a3662e

29 files changed

Lines changed: 1874 additions & 215 deletions
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import { describe, expect, it } from 'vitest';
2+
import {
3+
createArrowElements,
4+
createRoundedPath,
5+
createStraightPath,
6+
getEdgePathD,
7+
getLabelPosition,
8+
getMidPoint,
9+
getNodesAnchors,
10+
getPointAtT,
11+
getTangentAngle,
12+
} from '../../../../src/designs/utils';
13+
describe('geometry utils', () => {
14+
describe('getMidPoint', () => {
15+
it('should return null for empty points', () => {
16+
expect(getMidPoint([])).toBeNull();
17+
});
18+
19+
it('should return the midpoint of a line segment', () => {
20+
const result = getMidPoint([
21+
[0, 0],
22+
[10, 0],
23+
]);
24+
expect(result).toEqual([5, 0]);
25+
});
26+
27+
it('should return the midpoint of a multi-segment line', () => {
28+
// Total length = 10 + 10 = 20. Midpoint at distance 10.
29+
// Segment 1: (0,0)->(10,0) (len 10)
30+
// Segment 2: (10,0)->(10,10) (len 10)
31+
// Target is at end of segment 1 / start of segment 2
32+
const result = getMidPoint([
33+
[0, 0],
34+
[10, 0],
35+
[10, 10],
36+
]);
37+
expect(result).toEqual([10, 0]);
38+
});
39+
});
40+
41+
describe('createStraightPath', () => {
42+
it('should create a straight path string for 2 points', () => {
43+
const result = createStraightPath(
44+
[
45+
[0, 0],
46+
[10, 10],
47+
],
48+
0,
49+
0,
50+
);
51+
expect(result).toBe('M 0 0 L 10 10');
52+
});
53+
54+
it('should apply offset correctly', () => {
55+
const result = createStraightPath(
56+
[
57+
[0, 0],
58+
[10, 10],
59+
],
60+
5,
61+
5,
62+
);
63+
expect(result).toBe('M 5 5 L 15 15');
64+
});
65+
});
66+
67+
describe('createRoundedPath', () => {
68+
it('should act as straight line for 2 points ignoring radius', () => {
69+
const result = createRoundedPath(
70+
[
71+
[0, 0],
72+
[10, 0],
73+
],
74+
5,
75+
0,
76+
0,
77+
);
78+
expect(result).toBe('M 0 0 L 10 0');
79+
});
80+
81+
it('should create rounded corner for 3 points', () => {
82+
// (0,0) -> (10,0) -> (10,10)
83+
// Corner at (10,0). Radius 5.
84+
// Expected: Line to (5,0), Quad to (10,5), Line to (10,10)
85+
// result string should contain Q command
86+
const result = createRoundedPath(
87+
[
88+
[0, 0],
89+
[10, 0],
90+
[10, 10],
91+
],
92+
5,
93+
0,
94+
0,
95+
);
96+
expect(result).toContain('Q');
97+
expect(result).toContain('M 0 0');
98+
});
99+
});
100+
101+
describe('createArrowElements', () => {
102+
it('should create arrow type element', () => {
103+
const result = createArrowElements(0, 0, 0, 'arrow', 'black', 1, 10);
104+
expect(result).toHaveLength(1);
105+
expect(result[0]).toBeTruthy();
106+
});
107+
108+
it('should create triangle type element', () => {
109+
const result = createArrowElements(0, 0, 0, 'triangle', 'black', 1, 10);
110+
expect(result).toHaveLength(1);
111+
expect(result[0]).toBeTruthy();
112+
});
113+
});
114+
115+
describe('getNodesAnchors', () => {
116+
const node = { x: 0, y: 0, width: 100, height: 100 };
117+
118+
it('should return correct anchor points with default radio (0.25)', () => {
119+
const anchors = getNodesAnchors(node);
120+
expect(anchors.LC).toEqual({ x: 0, y: 50 });
121+
expect(anchors.RC).toEqual({ x: 100, y: 50 });
122+
expect(anchors.LT).toEqual({ x: 0, y: 25 });
123+
});
124+
125+
it('should return correct anchor points with custom radio', () => {
126+
const anchors = getNodesAnchors({ ...node, radio: 0.1 });
127+
expect(anchors.LT).toEqual({ x: 0, y: 10 });
128+
expect(anchors.LB).toEqual({ x: 0, y: 90 });
129+
});
130+
});
131+
132+
describe('getTangentAngle', () => {
133+
it('should return angle for straight line (2 points)', () => {
134+
const points: [number, number][] = [
135+
[0, 0],
136+
[10, 0],
137+
];
138+
expect(getTangentAngle(points, 1)).toBe(0);
139+
expect(getTangentAngle(points, 0)).toBe(0);
140+
});
141+
142+
it('should return angle for quadratic curve (3 points)', () => {
143+
const points: [number, number][] = [
144+
[0, 0],
145+
[5, 5],
146+
[10, 0],
147+
];
148+
expect(getTangentAngle(points, 0)).toBeCloseTo(Math.atan2(5, 5));
149+
expect(getTangentAngle(points, 1)).toBeCloseTo(Math.atan2(-5, 5));
150+
});
151+
});
152+
153+
describe('getPointAtT', () => {
154+
it('should return midpoint for straight line at t=0.5', () => {
155+
const points: [number, number][] = [
156+
[0, 0],
157+
[10, 10],
158+
];
159+
expect(getPointAtT(points, 0.5)).toEqual([5, 5]);
160+
});
161+
162+
it('should return correct point for quadratic curve at t=0.5', () => {
163+
const points: [number, number][] = [
164+
[0, 0],
165+
[10, 0],
166+
[20, 0],
167+
];
168+
expect(getPointAtT(points, 0.5)).toEqual([10, 0]);
169+
});
170+
});
171+
172+
describe('getLabelPosition', () => {
173+
it('should return midpoint for straight line (len=2)', () => {
174+
const points: [number, number][] = [
175+
[0, 0],
176+
[10, 0],
177+
];
178+
expect(getLabelPosition(points)).toEqual([5, 0]);
179+
});
180+
181+
it('should apply offset for self loop (len=4)', () => {
182+
const points: [number, number][] = [
183+
[0, 0],
184+
[0, 10],
185+
[10, 10],
186+
[10, 0],
187+
];
188+
const center = getPointAtT(points, 0.5);
189+
const result = getLabelPosition(points, 10);
190+
expect(result[0]).toBe(center[0] + 10);
191+
expect(result[1]).toBe(center[1]);
192+
});
193+
});
194+
195+
describe('getEdgePathD', () => {
196+
it('should return M L path for 2 points', () => {
197+
const points: [number, number][] = [
198+
[0, 0],
199+
[10, 10],
200+
];
201+
expect(getEdgePathD(points)).toBe('M 0 0 L 10 10');
202+
});
203+
204+
it('should return M Q path for 3 points', () => {
205+
const points: [number, number][] = [
206+
[0, 0],
207+
[5, 5],
208+
[10, 0],
209+
];
210+
expect(getEdgePathD(points)).toBe('M 0 0 Q 5 5 10 0');
211+
});
212+
});
213+
});
Lines changed: 1 addition & 1 deletion
Loading

__tests__/unit/ssr/output/02-list-with-icons.svg

Lines changed: 1 addition & 1 deletion
Loading

0 commit comments

Comments
 (0)