@antv/infographic 0.2.15 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +27 -0
- package/README.zh-CN.md +27 -0
- package/dist/infographic.min.js +112 -112
- package/dist/infographic.min.js.map +1 -1
- package/esm/designs/structures/sequence-interaction.js +36 -15
- package/esm/designs/structures/sequence-timeline.d.ts +1 -0
- package/esm/designs/structures/sequence-timeline.js +4 -2
- package/esm/exporter/png.js +2 -2
- package/esm/exporter/svg.js +9 -1
- package/esm/exporter/types.d.ts +10 -0
- package/esm/syntax/parser.js +80 -3
- package/esm/templates/built-in.js +2 -2
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/lib/designs/structures/sequence-interaction.js +36 -15
- package/lib/designs/structures/sequence-timeline.d.ts +1 -0
- package/lib/designs/structures/sequence-timeline.js +4 -2
- package/lib/exporter/png.js +2 -2
- package/lib/exporter/svg.js +9 -1
- package/lib/exporter/types.d.ts +10 -0
- package/lib/syntax/parser.js +80 -3
- package/lib/templates/built-in.js +2 -2
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/designs/structures/sequence-interaction.tsx +92 -46
- package/src/designs/structures/sequence-timeline.tsx +18 -15
- package/src/exporter/png.ts +3 -2
- package/src/exporter/svg.ts +14 -1
- package/src/exporter/types.ts +10 -0
- package/src/syntax/parser.ts +101 -3
- package/src/templates/built-in.ts +2 -2
- package/src/version.ts +1 -1
|
@@ -15,6 +15,7 @@ const DEFAULT_ITEM_HEIGHT = 50;
|
|
|
15
15
|
const FONT_SIZE = 14;
|
|
16
16
|
const ARROW_SIZE = 14;
|
|
17
17
|
const CORNER_RADIUS_NODE = 6;
|
|
18
|
+
const LIFELINE_MASK_GAP = 2;
|
|
18
19
|
const LANE_PADDING = 60;
|
|
19
20
|
const BTN_HALF_SIZE = 12;
|
|
20
21
|
const BTN_MARGIN = 10;
|
|
@@ -229,17 +230,6 @@ export const SequenceInteractionFlow = (props) => {
|
|
|
229
230
|
const decorElements = [];
|
|
230
231
|
const defsElements = [];
|
|
231
232
|
const btnElements = [];
|
|
232
|
-
// 绘制生命线
|
|
233
|
-
if (showLifeline) {
|
|
234
|
-
lanes.forEach((_lane, laneIndex) => {
|
|
235
|
-
const centerX = getLaneCenterX(laneIndex);
|
|
236
|
-
const startY = padding + headerOffset;
|
|
237
|
-
const endY = totalHeight - padding;
|
|
238
|
-
decorElements.push(_jsx(Path, { d: `M ${centerX} ${startY} L ${centerX} ${endY}`, stroke: colorBorder, strokeWidth: lifelineWidth, strokeDasharray: "5,5", fill: "none", "data-element-type": "shape" }));
|
|
239
|
-
// 绘制生命线末端箭头(实心)
|
|
240
|
-
decorElements.push(...createArrowElements(centerX, endY, Math.PI / 2, 'triangle', colorBorder, 1, 10));
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
233
|
// 绘制泳道标题
|
|
244
234
|
if (showLaneHeader) {
|
|
245
235
|
lanes.forEach((lane, laneIndex) => {
|
|
@@ -282,10 +272,6 @@ export const SequenceInteractionFlow = (props) => {
|
|
|
282
272
|
});
|
|
283
273
|
const nodeColor = getPaletteColor(options, [laneIndex]);
|
|
284
274
|
const nodeThemeColors = getThemeColors({ colorPrimary: nodeColor }, options);
|
|
285
|
-
// 添加节点背景遮挡层,防止生命线虚线透过半透明节点显示
|
|
286
|
-
// 只在节点中心放置窄条遮挡生命线,避免圆角处露出白色背景
|
|
287
|
-
const maskStripWidth = lifelineWidth + 6;
|
|
288
|
-
decorElements.push(_jsx(Rect, { x: centerX - maskStripWidth / 2, y: y, width: maskStripWidth, height: itemHeight, fill: colorBg }));
|
|
289
275
|
// 构造类似 hierarchy-tree 的 _originalIndex
|
|
290
276
|
const originalIndex = [laneIndex, rowIndex];
|
|
291
277
|
// 附加到数据上,确保 Item 组件能正确识别
|
|
@@ -322,6 +308,41 @@ export const SequenceInteractionFlow = (props) => {
|
|
|
322
308
|
const centerX = getLaneCenterX(laneIndex);
|
|
323
309
|
btnElements.push(_jsx(BtnAdd, { indexes: [laneIndex, childCount], x: centerX - BTN_HALF_SIZE, y: addNodeY }));
|
|
324
310
|
});
|
|
311
|
+
// 绘制生命线(使用 mask 挖空节点区域,避免虚线穿透半透明节点)
|
|
312
|
+
if (showLifeline) {
|
|
313
|
+
// 预先按泳道分组节点,避免每条泳道都遍历全部节点
|
|
314
|
+
const nodeRectsByLane = new Map();
|
|
315
|
+
nodeLayoutById.forEach((layout) => {
|
|
316
|
+
let list = nodeRectsByLane.get(layout.laneIndex);
|
|
317
|
+
if (!list) {
|
|
318
|
+
list = [];
|
|
319
|
+
nodeRectsByLane.set(layout.laneIndex, list);
|
|
320
|
+
}
|
|
321
|
+
list.push({
|
|
322
|
+
x: layout.x,
|
|
323
|
+
y: layout.y,
|
|
324
|
+
width: layout.width,
|
|
325
|
+
height: layout.height,
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
lanes.forEach((_lane, laneIndex) => {
|
|
329
|
+
var _a;
|
|
330
|
+
const centerX = getLaneCenterX(laneIndex);
|
|
331
|
+
const startY = padding + headerOffset;
|
|
332
|
+
const endY = totalHeight - padding;
|
|
333
|
+
const laneNodeRects = (_a = nodeRectsByLane.get(laneIndex)) !== null && _a !== void 0 ? _a : [];
|
|
334
|
+
// 如果该泳道有节点,创建 mask 来挖空节点区域
|
|
335
|
+
let lifelineMaskAttr;
|
|
336
|
+
if (laneNodeRects.length > 0) {
|
|
337
|
+
const maskId = `lifeline-mask-${instanceId}-${laneIndex}`;
|
|
338
|
+
defsElements.push(_jsxs("mask", { id: maskId, maskUnits: "userSpaceOnUse", x: 0, y: 0, width: totalWidth, height: totalHeight, children: [_jsx(Rect, { x: 0, y: 0, width: totalWidth, height: totalHeight, fill: "white" }), laneNodeRects.map((rect) => (_jsx(Rect, { x: rect.x, y: rect.y - LIFELINE_MASK_GAP, width: rect.width, height: rect.height + LIFELINE_MASK_GAP * 2, fill: "black" })))] }));
|
|
339
|
+
lifelineMaskAttr = `url(#${maskId})`;
|
|
340
|
+
}
|
|
341
|
+
decorElements.push(_jsx(Path, { d: `M ${centerX} ${startY} L ${centerX} ${endY}`, stroke: colorBorder, strokeWidth: lifelineWidth, strokeDasharray: "5,5", fill: "none", "data-element-type": "shape", mask: lifelineMaskAttr }));
|
|
342
|
+
// 绘制生命线末端箭头(实心)
|
|
343
|
+
decorElements.push(...createArrowElements(centerX, endY, Math.PI / 2, 'triangle', colorBorder, 1, 10));
|
|
344
|
+
});
|
|
345
|
+
}
|
|
325
346
|
// 添加新泳道按钮 (最右侧)
|
|
326
347
|
const lastLaneRightX = getLaneCenterX(lanes.length - 1) + laneWidth / 2;
|
|
327
348
|
const newLaneX = lanes.length > 0 ? lastLaneRightX + BTN_LANE_ADD_Gap : padding;
|
|
@@ -5,7 +5,7 @@ import { FlexLayout } from '../layouts/index.js';
|
|
|
5
5
|
import { getColorPrimary, getPaletteColor, getPaletteColors } from '../utils/index.js';
|
|
6
6
|
import { registerStructure } from './registry.js';
|
|
7
7
|
export const SequenceTimeline = (props) => {
|
|
8
|
-
const { Title, Item, data, gap = 10, options } = props;
|
|
8
|
+
const { Title, Item, data, gap = 10, showStepLabels = true, options } = props;
|
|
9
9
|
const { title, desc, items = [] } = data;
|
|
10
10
|
const titleContent = Title ? _jsx(Title, { title: title, desc: desc }) : null;
|
|
11
11
|
const colorPrimary = getColorPrimary(options);
|
|
@@ -41,7 +41,9 @@ export const SequenceTimeline = (props) => {
|
|
|
41
41
|
const itemY = index * (itemBounds.height + gap);
|
|
42
42
|
const nodeY = itemY + itemBounds.height / 2;
|
|
43
43
|
const indexes = [index];
|
|
44
|
-
|
|
44
|
+
if (showStepLabels) {
|
|
45
|
+
decorElements.push(_jsx(Text, { x: stepLabelX, y: nodeY, width: 70, fontSize: 18, fontWeight: "bold", alignHorizontal: "left", alignVertical: "middle", fill: palette[index % palette.length], children: `STEP ${index + 1}` }));
|
|
46
|
+
}
|
|
45
47
|
itemElements.push(_jsx(Item, { indexes: indexes, datum: item, data: data, x: itemX, y: itemY, positionH: "normal" }));
|
|
46
48
|
decorElements.push(_jsx(Ellipse, { x: timelineX - nodeRadius, y: nodeY - nodeRadius, width: nodeRadius * 2, height: nodeRadius * 2, fill: palette[index % palette.length] }));
|
|
47
49
|
btnElements.push(_jsx(BtnRemove, { indexes: indexes, x: itemX - btnBounds.width - 10, y: itemY + (itemBounds.height - btnBounds.height) / 2 }));
|
package/esm/exporter/png.js
CHANGED
|
@@ -12,8 +12,8 @@ import { exportToSVG } from './svg.js';
|
|
|
12
12
|
export function exportToPNGString(svg_1) {
|
|
13
13
|
return __awaiter(this, arguments, void 0, function* (svg, options = {}) {
|
|
14
14
|
var _a;
|
|
15
|
-
const { dpr = (_a = globalThis.devicePixelRatio) !== null && _a !== void 0 ? _a : 2 } = options;
|
|
16
|
-
const node = yield exportToSVG(svg);
|
|
15
|
+
const { dpr = (_a = globalThis.devicePixelRatio) !== null && _a !== void 0 ? _a : 2, removeBackground = false } = options;
|
|
16
|
+
const node = yield exportToSVG(svg, { removeBackground });
|
|
17
17
|
const { width, height } = getViewBox(node);
|
|
18
18
|
return new Promise((resolve, reject) => {
|
|
19
19
|
try {
|
package/esm/exporter/svg.js
CHANGED
|
@@ -18,7 +18,7 @@ export function exportToSVGString(svg_1) {
|
|
|
18
18
|
}
|
|
19
19
|
export function exportToSVG(svg_1) {
|
|
20
20
|
return __awaiter(this, arguments, void 0, function* (svg, options = {}) {
|
|
21
|
-
const { embedResources = true, removeIds = false } = options;
|
|
21
|
+
const { removeBackground = false, embedResources = true, removeIds = false, } = options;
|
|
22
22
|
const clonedSVG = svg.cloneNode(true);
|
|
23
23
|
const { width, height } = getViewBox(svg);
|
|
24
24
|
setAttributes(clonedSVG, { width, height });
|
|
@@ -30,6 +30,9 @@ export function exportToSVG(svg_1) {
|
|
|
30
30
|
yield embedIcons(clonedSVG);
|
|
31
31
|
}
|
|
32
32
|
yield embedFonts(clonedSVG, embedResources);
|
|
33
|
+
if (removeBackground) {
|
|
34
|
+
removeSVGBackground(clonedSVG);
|
|
35
|
+
}
|
|
33
36
|
cleanSVG(clonedSVG);
|
|
34
37
|
return clonedSVG;
|
|
35
38
|
});
|
|
@@ -284,6 +287,11 @@ function cleanSVG(svg) {
|
|
|
284
287
|
removeUselessAttrs(svg);
|
|
285
288
|
clearDataset(svg);
|
|
286
289
|
}
|
|
290
|
+
function removeSVGBackground(svg) {
|
|
291
|
+
svg.style.removeProperty('background-color');
|
|
292
|
+
const background = getElementByRole(svg, "background" /* ElementTypeEnum.Background */);
|
|
293
|
+
background === null || background === void 0 ? void 0 : background.remove();
|
|
294
|
+
}
|
|
287
295
|
function removeBtnGroup(svg) {
|
|
288
296
|
const btnGroup = getElementByRole(svg, "btns-group" /* ElementTypeEnum.BtnsGroup */);
|
|
289
297
|
btnGroup === null || btnGroup === void 0 ? void 0 : btnGroup.remove();
|
package/esm/exporter/types.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export interface SVGExportOptions {
|
|
2
2
|
type: 'svg';
|
|
3
|
+
/**
|
|
4
|
+
* 是否移除背景(SVG 背景样式 + 背景矩形)
|
|
5
|
+
* @default false
|
|
6
|
+
*/
|
|
7
|
+
removeBackground?: boolean;
|
|
3
8
|
/**
|
|
4
9
|
* 是否将远程资源嵌入到 SVG 中
|
|
5
10
|
* @default true
|
|
@@ -13,6 +18,11 @@ export interface SVGExportOptions {
|
|
|
13
18
|
}
|
|
14
19
|
export interface PNGExportOptions {
|
|
15
20
|
type: 'png';
|
|
21
|
+
/**
|
|
22
|
+
* 是否移除背景(SVG 背景样式 + 背景矩形)
|
|
23
|
+
* @default false
|
|
24
|
+
*/
|
|
25
|
+
removeBackground?: boolean;
|
|
16
26
|
/**
|
|
17
27
|
* 设备像素比,默认为浏览器的 devicePixelRatio
|
|
18
28
|
* @default globalThis.devicePixelRatio || 2
|
package/esm/syntax/parser.js
CHANGED
|
@@ -40,6 +40,81 @@ function parseKeyValue(raw) {
|
|
|
40
40
|
}
|
|
41
41
|
return { key: text, value: undefined };
|
|
42
42
|
}
|
|
43
|
+
function isUnsafeObjectKey(key) {
|
|
44
|
+
return key === '__proto__' || key === 'constructor' || key === 'prototype';
|
|
45
|
+
}
|
|
46
|
+
function assignObjectEntry(parent, rawKey, node, line, errors) {
|
|
47
|
+
if (!rawKey.includes('.')) {
|
|
48
|
+
if (isUnsafeObjectKey(rawKey)) {
|
|
49
|
+
errors.push({
|
|
50
|
+
path: rawKey,
|
|
51
|
+
line,
|
|
52
|
+
code: 'bad_syntax',
|
|
53
|
+
message: `Invalid key part: ${rawKey}`,
|
|
54
|
+
raw: rawKey,
|
|
55
|
+
});
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
parent.entries[rawKey] = node;
|
|
59
|
+
return { parent, key: rawKey };
|
|
60
|
+
}
|
|
61
|
+
const parts = rawKey.split('.');
|
|
62
|
+
if (parts.some((part) => !part)) {
|
|
63
|
+
errors.push({
|
|
64
|
+
path: rawKey,
|
|
65
|
+
line,
|
|
66
|
+
code: 'bad_syntax',
|
|
67
|
+
message: 'Invalid dotted key path.',
|
|
68
|
+
raw: rawKey,
|
|
69
|
+
});
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
let current = parent;
|
|
73
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
74
|
+
const part = parts[index];
|
|
75
|
+
if (isUnsafeObjectKey(part)) {
|
|
76
|
+
errors.push({
|
|
77
|
+
path: rawKey,
|
|
78
|
+
line,
|
|
79
|
+
code: 'bad_syntax',
|
|
80
|
+
message: `Invalid key part in dotted path: ${part}`,
|
|
81
|
+
raw: rawKey,
|
|
82
|
+
});
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
const existing = current.entries[part];
|
|
86
|
+
if (!existing) {
|
|
87
|
+
const container = createObjectNode(line);
|
|
88
|
+
current.entries[part] = container;
|
|
89
|
+
current = container;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (existing.kind !== 'object') {
|
|
93
|
+
errors.push({
|
|
94
|
+
path: parts.slice(0, index + 1).join('.'),
|
|
95
|
+
line,
|
|
96
|
+
code: 'bad_syntax',
|
|
97
|
+
message: 'Cannot assign dotted key under a list value.',
|
|
98
|
+
raw: rawKey,
|
|
99
|
+
});
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
current = existing;
|
|
103
|
+
}
|
|
104
|
+
const finalKey = parts[parts.length - 1];
|
|
105
|
+
if (isUnsafeObjectKey(finalKey)) {
|
|
106
|
+
errors.push({
|
|
107
|
+
path: rawKey,
|
|
108
|
+
line,
|
|
109
|
+
code: 'bad_syntax',
|
|
110
|
+
message: `Invalid key part in dotted path: ${finalKey}`,
|
|
111
|
+
raw: rawKey,
|
|
112
|
+
});
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
current.entries[finalKey] = node;
|
|
116
|
+
return { parent: current, key: finalKey };
|
|
117
|
+
}
|
|
43
118
|
function createObjectNode(line, value) {
|
|
44
119
|
return { kind: 'object', line, value, entries: {} };
|
|
45
120
|
}
|
|
@@ -171,12 +246,14 @@ export function parseSyntaxToAst(input) {
|
|
|
171
246
|
return;
|
|
172
247
|
}
|
|
173
248
|
const node = createObjectNode(lineNumber, parsed.value);
|
|
174
|
-
parentNode
|
|
249
|
+
const assigned = assignObjectEntry(parentNode, parsed.key, node, lineNumber, errors);
|
|
250
|
+
if (!assigned)
|
|
251
|
+
return;
|
|
175
252
|
stack.push({
|
|
176
253
|
indent,
|
|
177
254
|
node,
|
|
178
|
-
parent:
|
|
179
|
-
key:
|
|
255
|
+
parent: assigned.parent,
|
|
256
|
+
key: assigned.key,
|
|
180
257
|
});
|
|
181
258
|
});
|
|
182
259
|
return { ast: root, errors };
|
|
@@ -181,7 +181,7 @@ const BUILT_IN_TEMPLATES = Object.assign(Object.assign(Object.assign(Object.assi
|
|
|
181
181
|
}, 'sequence-timeline-plain-text': {
|
|
182
182
|
design: {
|
|
183
183
|
title: 'default',
|
|
184
|
-
structure: { type: 'sequence-timeline' },
|
|
184
|
+
structure: { type: 'sequence-timeline', showStepLabels: false },
|
|
185
185
|
items: [{ type: 'plain-text' }],
|
|
186
186
|
},
|
|
187
187
|
}, 'sequence-timeline-rounded-rect-node': {
|
|
@@ -199,7 +199,7 @@ const BUILT_IN_TEMPLATES = Object.assign(Object.assign(Object.assign(Object.assi
|
|
|
199
199
|
}, 'sequence-timeline-simple': {
|
|
200
200
|
design: {
|
|
201
201
|
title: 'default',
|
|
202
|
-
structure: { type: 'sequence-timeline', gap: 20 },
|
|
202
|
+
structure: { type: 'sequence-timeline', gap: 20, showStepLabels: false },
|
|
203
203
|
items: [{ type: 'simple', positionV: 'middle' }],
|
|
204
204
|
},
|
|
205
205
|
}, 'sequence-cylinders-3d-simple': {
|
package/esm/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.2.
|
|
1
|
+
export declare const VERSION = "0.2.16";
|
package/esm/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const VERSION = '0.2.
|
|
1
|
+
export const VERSION = '0.2.16';
|
|
@@ -18,6 +18,7 @@ const DEFAULT_ITEM_HEIGHT = 50;
|
|
|
18
18
|
const FONT_SIZE = 14;
|
|
19
19
|
const ARROW_SIZE = 14;
|
|
20
20
|
const CORNER_RADIUS_NODE = 6;
|
|
21
|
+
const LIFELINE_MASK_GAP = 2;
|
|
21
22
|
const LANE_PADDING = 60;
|
|
22
23
|
const BTN_HALF_SIZE = 12;
|
|
23
24
|
const BTN_MARGIN = 10;
|
|
@@ -232,17 +233,6 @@ const SequenceInteractionFlow = (props) => {
|
|
|
232
233
|
const decorElements = [];
|
|
233
234
|
const defsElements = [];
|
|
234
235
|
const btnElements = [];
|
|
235
|
-
// 绘制生命线
|
|
236
|
-
if (showLifeline) {
|
|
237
|
-
lanes.forEach((_lane, laneIndex) => {
|
|
238
|
-
const centerX = getLaneCenterX(laneIndex);
|
|
239
|
-
const startY = padding + headerOffset;
|
|
240
|
-
const endY = totalHeight - padding;
|
|
241
|
-
decorElements.push((0, jsx_runtime_1.jsx)(jsx_1.Path, { d: `M ${centerX} ${startY} L ${centerX} ${endY}`, stroke: colorBorder, strokeWidth: lifelineWidth, strokeDasharray: "5,5", fill: "none", "data-element-type": "shape" }));
|
|
242
|
-
// 绘制生命线末端箭头(实心)
|
|
243
|
-
decorElements.push(...(0, utils_1.createArrowElements)(centerX, endY, Math.PI / 2, 'triangle', colorBorder, 1, 10));
|
|
244
|
-
});
|
|
245
|
-
}
|
|
246
236
|
// 绘制泳道标题
|
|
247
237
|
if (showLaneHeader) {
|
|
248
238
|
lanes.forEach((lane, laneIndex) => {
|
|
@@ -285,10 +275,6 @@ const SequenceInteractionFlow = (props) => {
|
|
|
285
275
|
});
|
|
286
276
|
const nodeColor = (0, utils_1.getPaletteColor)(options, [laneIndex]);
|
|
287
277
|
const nodeThemeColors = (0, utils_1.getThemeColors)({ colorPrimary: nodeColor }, options);
|
|
288
|
-
// 添加节点背景遮挡层,防止生命线虚线透过半透明节点显示
|
|
289
|
-
// 只在节点中心放置窄条遮挡生命线,避免圆角处露出白色背景
|
|
290
|
-
const maskStripWidth = lifelineWidth + 6;
|
|
291
|
-
decorElements.push((0, jsx_runtime_1.jsx)(jsx_1.Rect, { x: centerX - maskStripWidth / 2, y: y, width: maskStripWidth, height: itemHeight, fill: colorBg }));
|
|
292
278
|
// 构造类似 hierarchy-tree 的 _originalIndex
|
|
293
279
|
const originalIndex = [laneIndex, rowIndex];
|
|
294
280
|
// 附加到数据上,确保 Item 组件能正确识别
|
|
@@ -325,6 +311,41 @@ const SequenceInteractionFlow = (props) => {
|
|
|
325
311
|
const centerX = getLaneCenterX(laneIndex);
|
|
326
312
|
btnElements.push((0, jsx_runtime_1.jsx)(components_1.BtnAdd, { indexes: [laneIndex, childCount], x: centerX - BTN_HALF_SIZE, y: addNodeY }));
|
|
327
313
|
});
|
|
314
|
+
// 绘制生命线(使用 mask 挖空节点区域,避免虚线穿透半透明节点)
|
|
315
|
+
if (showLifeline) {
|
|
316
|
+
// 预先按泳道分组节点,避免每条泳道都遍历全部节点
|
|
317
|
+
const nodeRectsByLane = new Map();
|
|
318
|
+
nodeLayoutById.forEach((layout) => {
|
|
319
|
+
let list = nodeRectsByLane.get(layout.laneIndex);
|
|
320
|
+
if (!list) {
|
|
321
|
+
list = [];
|
|
322
|
+
nodeRectsByLane.set(layout.laneIndex, list);
|
|
323
|
+
}
|
|
324
|
+
list.push({
|
|
325
|
+
x: layout.x,
|
|
326
|
+
y: layout.y,
|
|
327
|
+
width: layout.width,
|
|
328
|
+
height: layout.height,
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
lanes.forEach((_lane, laneIndex) => {
|
|
332
|
+
var _a;
|
|
333
|
+
const centerX = getLaneCenterX(laneIndex);
|
|
334
|
+
const startY = padding + headerOffset;
|
|
335
|
+
const endY = totalHeight - padding;
|
|
336
|
+
const laneNodeRects = (_a = nodeRectsByLane.get(laneIndex)) !== null && _a !== void 0 ? _a : [];
|
|
337
|
+
// 如果该泳道有节点,创建 mask 来挖空节点区域
|
|
338
|
+
let lifelineMaskAttr;
|
|
339
|
+
if (laneNodeRects.length > 0) {
|
|
340
|
+
const maskId = `lifeline-mask-${instanceId}-${laneIndex}`;
|
|
341
|
+
defsElements.push((0, jsx_runtime_1.jsxs)("mask", { id: maskId, maskUnits: "userSpaceOnUse", x: 0, y: 0, width: totalWidth, height: totalHeight, children: [(0, jsx_runtime_1.jsx)(jsx_1.Rect, { x: 0, y: 0, width: totalWidth, height: totalHeight, fill: "white" }), laneNodeRects.map((rect) => ((0, jsx_runtime_1.jsx)(jsx_1.Rect, { x: rect.x, y: rect.y - LIFELINE_MASK_GAP, width: rect.width, height: rect.height + LIFELINE_MASK_GAP * 2, fill: "black" })))] }));
|
|
342
|
+
lifelineMaskAttr = `url(#${maskId})`;
|
|
343
|
+
}
|
|
344
|
+
decorElements.push((0, jsx_runtime_1.jsx)(jsx_1.Path, { d: `M ${centerX} ${startY} L ${centerX} ${endY}`, stroke: colorBorder, strokeWidth: lifelineWidth, strokeDasharray: "5,5", fill: "none", "data-element-type": "shape", mask: lifelineMaskAttr }));
|
|
345
|
+
// 绘制生命线末端箭头(实心)
|
|
346
|
+
decorElements.push(...(0, utils_1.createArrowElements)(centerX, endY, Math.PI / 2, 'triangle', colorBorder, 1, 10));
|
|
347
|
+
});
|
|
348
|
+
}
|
|
328
349
|
// 添加新泳道按钮 (最右侧)
|
|
329
350
|
const lastLaneRightX = getLaneCenterX(lanes.length - 1) + laneWidth / 2;
|
|
330
351
|
const newLaneX = lanes.length > 0 ? lastLaneRightX + BTN_LANE_ADD_Gap : padding;
|
|
@@ -8,7 +8,7 @@ const layouts_1 = require("../layouts");
|
|
|
8
8
|
const utils_1 = require("../utils");
|
|
9
9
|
const registry_1 = require("./registry");
|
|
10
10
|
const SequenceTimeline = (props) => {
|
|
11
|
-
const { Title, Item, data, gap = 10, options } = props;
|
|
11
|
+
const { Title, Item, data, gap = 10, showStepLabels = true, options } = props;
|
|
12
12
|
const { title, desc, items = [] } = data;
|
|
13
13
|
const titleContent = Title ? (0, jsx_runtime_1.jsx)(Title, { title: title, desc: desc }) : null;
|
|
14
14
|
const colorPrimary = (0, utils_1.getColorPrimary)(options);
|
|
@@ -44,7 +44,9 @@ const SequenceTimeline = (props) => {
|
|
|
44
44
|
const itemY = index * (itemBounds.height + gap);
|
|
45
45
|
const nodeY = itemY + itemBounds.height / 2;
|
|
46
46
|
const indexes = [index];
|
|
47
|
-
|
|
47
|
+
if (showStepLabels) {
|
|
48
|
+
decorElements.push((0, jsx_runtime_1.jsx)(jsx_1.Text, { x: stepLabelX, y: nodeY, width: 70, fontSize: 18, fontWeight: "bold", alignHorizontal: "left", alignVertical: "middle", fill: palette[index % palette.length], children: `STEP ${index + 1}` }));
|
|
49
|
+
}
|
|
48
50
|
itemElements.push((0, jsx_runtime_1.jsx)(Item, { indexes: indexes, datum: item, data: data, x: itemX, y: itemY, positionH: "normal" }));
|
|
49
51
|
decorElements.push((0, jsx_runtime_1.jsx)(jsx_1.Ellipse, { x: timelineX - nodeRadius, y: nodeY - nodeRadius, width: nodeRadius * 2, height: nodeRadius * 2, fill: palette[index % palette.length] }));
|
|
50
52
|
btnElements.push((0, jsx_runtime_1.jsx)(components_1.BtnRemove, { indexes: indexes, x: itemX - btnBounds.width - 10, y: itemY + (itemBounds.height - btnBounds.height) / 2 }));
|
package/lib/exporter/png.js
CHANGED
|
@@ -15,8 +15,8 @@ const svg_1 = require("./svg");
|
|
|
15
15
|
function exportToPNGString(svg_2) {
|
|
16
16
|
return __awaiter(this, arguments, void 0, function* (svg, options = {}) {
|
|
17
17
|
var _a;
|
|
18
|
-
const { dpr = (_a = globalThis.devicePixelRatio) !== null && _a !== void 0 ? _a : 2 } = options;
|
|
19
|
-
const node = yield (0, svg_1.exportToSVG)(svg);
|
|
18
|
+
const { dpr = (_a = globalThis.devicePixelRatio) !== null && _a !== void 0 ? _a : 2, removeBackground = false } = options;
|
|
19
|
+
const node = yield (0, svg_1.exportToSVG)(svg, { removeBackground });
|
|
20
20
|
const { width, height } = (0, utils_1.getViewBox)(node);
|
|
21
21
|
return new Promise((resolve, reject) => {
|
|
22
22
|
try {
|
package/lib/exporter/svg.js
CHANGED
|
@@ -22,7 +22,7 @@ function exportToSVGString(svg_1) {
|
|
|
22
22
|
}
|
|
23
23
|
function exportToSVG(svg_1) {
|
|
24
24
|
return __awaiter(this, arguments, void 0, function* (svg, options = {}) {
|
|
25
|
-
const { embedResources = true, removeIds = false } = options;
|
|
25
|
+
const { removeBackground = false, embedResources = true, removeIds = false, } = options;
|
|
26
26
|
const clonedSVG = svg.cloneNode(true);
|
|
27
27
|
const { width, height } = (0, utils_1.getViewBox)(svg);
|
|
28
28
|
(0, utils_1.setAttributes)(clonedSVG, { width, height });
|
|
@@ -34,6 +34,9 @@ function exportToSVG(svg_1) {
|
|
|
34
34
|
yield embedIcons(clonedSVG);
|
|
35
35
|
}
|
|
36
36
|
yield (0, font_1.embedFonts)(clonedSVG, embedResources);
|
|
37
|
+
if (removeBackground) {
|
|
38
|
+
removeSVGBackground(clonedSVG);
|
|
39
|
+
}
|
|
37
40
|
cleanSVG(clonedSVG);
|
|
38
41
|
return clonedSVG;
|
|
39
42
|
});
|
|
@@ -288,6 +291,11 @@ function cleanSVG(svg) {
|
|
|
288
291
|
removeUselessAttrs(svg);
|
|
289
292
|
clearDataset(svg);
|
|
290
293
|
}
|
|
294
|
+
function removeSVGBackground(svg) {
|
|
295
|
+
svg.style.removeProperty('background-color');
|
|
296
|
+
const background = (0, utils_1.getElementByRole)(svg, "background" /* ElementTypeEnum.Background */);
|
|
297
|
+
background === null || background === void 0 ? void 0 : background.remove();
|
|
298
|
+
}
|
|
291
299
|
function removeBtnGroup(svg) {
|
|
292
300
|
const btnGroup = (0, utils_1.getElementByRole)(svg, "btns-group" /* ElementTypeEnum.BtnsGroup */);
|
|
293
301
|
btnGroup === null || btnGroup === void 0 ? void 0 : btnGroup.remove();
|
package/lib/exporter/types.d.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
export interface SVGExportOptions {
|
|
2
2
|
type: 'svg';
|
|
3
|
+
/**
|
|
4
|
+
* 是否移除背景(SVG 背景样式 + 背景矩形)
|
|
5
|
+
* @default false
|
|
6
|
+
*/
|
|
7
|
+
removeBackground?: boolean;
|
|
3
8
|
/**
|
|
4
9
|
* 是否将远程资源嵌入到 SVG 中
|
|
5
10
|
* @default true
|
|
@@ -13,6 +18,11 @@ export interface SVGExportOptions {
|
|
|
13
18
|
}
|
|
14
19
|
export interface PNGExportOptions {
|
|
15
20
|
type: 'png';
|
|
21
|
+
/**
|
|
22
|
+
* 是否移除背景(SVG 背景样式 + 背景矩形)
|
|
23
|
+
* @default false
|
|
24
|
+
*/
|
|
25
|
+
removeBackground?: boolean;
|
|
16
26
|
/**
|
|
17
27
|
* 设备像素比,默认为浏览器的 devicePixelRatio
|
|
18
28
|
* @default globalThis.devicePixelRatio || 2
|
package/lib/syntax/parser.js
CHANGED
|
@@ -44,6 +44,81 @@ function parseKeyValue(raw) {
|
|
|
44
44
|
}
|
|
45
45
|
return { key: text, value: undefined };
|
|
46
46
|
}
|
|
47
|
+
function isUnsafeObjectKey(key) {
|
|
48
|
+
return key === '__proto__' || key === 'constructor' || key === 'prototype';
|
|
49
|
+
}
|
|
50
|
+
function assignObjectEntry(parent, rawKey, node, line, errors) {
|
|
51
|
+
if (!rawKey.includes('.')) {
|
|
52
|
+
if (isUnsafeObjectKey(rawKey)) {
|
|
53
|
+
errors.push({
|
|
54
|
+
path: rawKey,
|
|
55
|
+
line,
|
|
56
|
+
code: 'bad_syntax',
|
|
57
|
+
message: `Invalid key part: ${rawKey}`,
|
|
58
|
+
raw: rawKey,
|
|
59
|
+
});
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
parent.entries[rawKey] = node;
|
|
63
|
+
return { parent, key: rawKey };
|
|
64
|
+
}
|
|
65
|
+
const parts = rawKey.split('.');
|
|
66
|
+
if (parts.some((part) => !part)) {
|
|
67
|
+
errors.push({
|
|
68
|
+
path: rawKey,
|
|
69
|
+
line,
|
|
70
|
+
code: 'bad_syntax',
|
|
71
|
+
message: 'Invalid dotted key path.',
|
|
72
|
+
raw: rawKey,
|
|
73
|
+
});
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
let current = parent;
|
|
77
|
+
for (let index = 0; index < parts.length - 1; index += 1) {
|
|
78
|
+
const part = parts[index];
|
|
79
|
+
if (isUnsafeObjectKey(part)) {
|
|
80
|
+
errors.push({
|
|
81
|
+
path: rawKey,
|
|
82
|
+
line,
|
|
83
|
+
code: 'bad_syntax',
|
|
84
|
+
message: `Invalid key part in dotted path: ${part}`,
|
|
85
|
+
raw: rawKey,
|
|
86
|
+
});
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const existing = current.entries[part];
|
|
90
|
+
if (!existing) {
|
|
91
|
+
const container = createObjectNode(line);
|
|
92
|
+
current.entries[part] = container;
|
|
93
|
+
current = container;
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (existing.kind !== 'object') {
|
|
97
|
+
errors.push({
|
|
98
|
+
path: parts.slice(0, index + 1).join('.'),
|
|
99
|
+
line,
|
|
100
|
+
code: 'bad_syntax',
|
|
101
|
+
message: 'Cannot assign dotted key under a list value.',
|
|
102
|
+
raw: rawKey,
|
|
103
|
+
});
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
current = existing;
|
|
107
|
+
}
|
|
108
|
+
const finalKey = parts[parts.length - 1];
|
|
109
|
+
if (isUnsafeObjectKey(finalKey)) {
|
|
110
|
+
errors.push({
|
|
111
|
+
path: rawKey,
|
|
112
|
+
line,
|
|
113
|
+
code: 'bad_syntax',
|
|
114
|
+
message: `Invalid key part in dotted path: ${finalKey}`,
|
|
115
|
+
raw: rawKey,
|
|
116
|
+
});
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
current.entries[finalKey] = node;
|
|
120
|
+
return { parent: current, key: finalKey };
|
|
121
|
+
}
|
|
47
122
|
function createObjectNode(line, value) {
|
|
48
123
|
return { kind: 'object', line, value, entries: {} };
|
|
49
124
|
}
|
|
@@ -175,12 +250,14 @@ function parseSyntaxToAst(input) {
|
|
|
175
250
|
return;
|
|
176
251
|
}
|
|
177
252
|
const node = createObjectNode(lineNumber, parsed.value);
|
|
178
|
-
parentNode
|
|
253
|
+
const assigned = assignObjectEntry(parentNode, parsed.key, node, lineNumber, errors);
|
|
254
|
+
if (!assigned)
|
|
255
|
+
return;
|
|
179
256
|
stack.push({
|
|
180
257
|
indent,
|
|
181
258
|
node,
|
|
182
|
-
parent:
|
|
183
|
-
key:
|
|
259
|
+
parent: assigned.parent,
|
|
260
|
+
key: assigned.key,
|
|
184
261
|
});
|
|
185
262
|
});
|
|
186
263
|
return { ast: root, errors };
|
|
@@ -183,7 +183,7 @@ const BUILT_IN_TEMPLATES = Object.assign(Object.assign(Object.assign(Object.assi
|
|
|
183
183
|
}, 'sequence-timeline-plain-text': {
|
|
184
184
|
design: {
|
|
185
185
|
title: 'default',
|
|
186
|
-
structure: { type: 'sequence-timeline' },
|
|
186
|
+
structure: { type: 'sequence-timeline', showStepLabels: false },
|
|
187
187
|
items: [{ type: 'plain-text' }],
|
|
188
188
|
},
|
|
189
189
|
}, 'sequence-timeline-rounded-rect-node': {
|
|
@@ -201,7 +201,7 @@ const BUILT_IN_TEMPLATES = Object.assign(Object.assign(Object.assign(Object.assi
|
|
|
201
201
|
}, 'sequence-timeline-simple': {
|
|
202
202
|
design: {
|
|
203
203
|
title: 'default',
|
|
204
|
-
structure: { type: 'sequence-timeline', gap: 20 },
|
|
204
|
+
structure: { type: 'sequence-timeline', gap: 20, showStepLabels: false },
|
|
205
205
|
items: [{ type: 'simple', positionV: 'middle' }],
|
|
206
206
|
},
|
|
207
207
|
}, 'sequence-cylinders-3d-simple': {
|
package/lib/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const VERSION = "0.2.
|
|
1
|
+
export declare const VERSION = "0.2.16";
|
package/lib/version.js
CHANGED