@antv/infographic 0.2.14 → 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 +39 -5
- package/README.zh-CN.md +39 -5
- package/dist/infographic.min.js +168 -166
- package/dist/infographic.min.js.map +1 -1
- package/esm/designs/structures/index.d.ts +1 -0
- package/esm/designs/structures/index.js +1 -0
- package/esm/designs/structures/relation-dagre-flow.js +4 -139
- package/esm/designs/structures/sequence-interaction.d.ts +54 -0
- package/esm/designs/structures/sequence-interaction.js +461 -0
- package/esm/designs/structures/sequence-timeline.d.ts +1 -0
- package/esm/designs/structures/sequence-timeline.js +4 -2
- package/esm/designs/utils/geometry.d.ts +44 -0
- package/esm/designs/utils/geometry.js +244 -0
- package/esm/designs/utils/index.d.ts +1 -0
- package/esm/designs/utils/index.js +1 -0
- package/esm/editor/managers/sync-registry.d.ts +2 -1
- package/esm/editor/types/editor.d.ts +2 -1
- package/esm/editor/types/sync.d.ts +2 -1
- package/esm/editor/utils/object.js +46 -39
- package/esm/exporter/png.js +2 -2
- package/esm/exporter/svg.js +9 -1
- package/esm/exporter/types.d.ts +10 -0
- package/esm/options/types.d.ts +6 -0
- package/esm/runtime/Infographic.js +20 -7
- package/esm/syntax/index.js +40 -20
- package/esm/syntax/parser.js +80 -3
- package/esm/syntax/relations.js +26 -2
- package/esm/syntax/schema.js +1 -0
- package/esm/templates/built-in.js +5 -4
- package/esm/templates/sequence-interaction.d.ts +2 -0
- package/esm/templates/sequence-interaction.js +76 -0
- package/esm/types/data.d.ts +1 -0
- package/esm/utils/index.d.ts +1 -0
- package/esm/utils/index.js +1 -0
- package/esm/utils/measure-text.js +31 -3
- package/esm/utils/types.d.ts +16 -0
- package/esm/utils/types.js +12 -0
- package/esm/version.d.ts +1 -1
- package/esm/version.js +1 -1
- package/lib/designs/structures/index.d.ts +1 -0
- package/lib/designs/structures/index.js +1 -0
- package/lib/designs/structures/relation-dagre-flow.js +5 -140
- package/lib/designs/structures/sequence-interaction.d.ts +54 -0
- package/lib/designs/structures/sequence-interaction.js +465 -0
- package/lib/designs/structures/sequence-timeline.d.ts +1 -0
- package/lib/designs/structures/sequence-timeline.js +4 -2
- package/lib/designs/utils/geometry.d.ts +44 -0
- package/lib/designs/utils/geometry.js +256 -0
- package/lib/designs/utils/index.d.ts +1 -0
- package/lib/designs/utils/index.js +1 -0
- package/lib/editor/managers/sync-registry.d.ts +2 -1
- package/lib/editor/types/editor.d.ts +2 -1
- package/lib/editor/types/sync.d.ts +2 -1
- package/lib/editor/utils/object.js +45 -38
- package/lib/exporter/png.js +2 -2
- package/lib/exporter/svg.js +9 -1
- package/lib/exporter/types.d.ts +10 -0
- package/lib/options/types.d.ts +6 -0
- package/lib/runtime/Infographic.js +19 -6
- package/lib/syntax/index.js +40 -20
- package/lib/syntax/parser.js +80 -3
- package/lib/syntax/relations.js +26 -2
- package/lib/syntax/schema.js +1 -0
- package/lib/templates/built-in.js +5 -4
- package/lib/templates/sequence-interaction.d.ts +2 -0
- package/lib/templates/sequence-interaction.js +79 -0
- package/lib/types/data.d.ts +1 -0
- package/lib/utils/index.d.ts +1 -0
- package/lib/utils/index.js +1 -0
- package/lib/utils/measure-text.js +30 -2
- package/lib/utils/types.d.ts +16 -0
- package/lib/utils/types.js +13 -0
- package/lib/version.d.ts +1 -1
- package/lib/version.js +1 -1
- package/package.json +1 -1
- package/src/designs/structures/index.ts +1 -0
- package/src/designs/structures/relation-dagre-flow.tsx +14 -178
- package/src/designs/structures/sequence-interaction.tsx +931 -0
- package/src/designs/structures/sequence-timeline.tsx +18 -15
- package/src/designs/utils/geometry.tsx +315 -0
- package/src/designs/utils/index.ts +1 -0
- package/src/editor/managers/sync-registry.ts +2 -1
- package/src/editor/types/editor.ts +2 -1
- package/src/editor/types/sync.ts +3 -1
- package/src/editor/utils/object.ts +50 -40
- package/src/exporter/png.ts +3 -2
- package/src/exporter/svg.ts +14 -1
- package/src/exporter/types.ts +10 -0
- package/src/options/types.ts +7 -0
- package/src/runtime/Infographic.tsx +27 -17
- package/src/syntax/index.ts +51 -18
- package/src/syntax/parser.ts +101 -3
- package/src/syntax/relations.ts +29 -2
- package/src/syntax/schema.ts +1 -0
- package/src/templates/built-in.ts +4 -2
- package/src/templates/sequence-interaction.ts +101 -0
- package/src/types/data.ts +1 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/measure-text.ts +35 -3
- package/src/utils/types.ts +61 -0
- package/src/version.ts +1 -1
|
@@ -15,12 +15,13 @@ import type { BaseStructureProps } from './types';
|
|
|
15
15
|
export interface SequenceTimelineProps extends BaseStructureProps {
|
|
16
16
|
gap?: number;
|
|
17
17
|
lineOffset?: number;
|
|
18
|
+
showStepLabels?: boolean;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
21
|
export const SequenceTimeline: ComponentType<SequenceTimelineProps> = (
|
|
21
22
|
props,
|
|
22
23
|
) => {
|
|
23
|
-
const { Title, Item, data, gap = 10, options } = props;
|
|
24
|
+
const { Title, Item, data, gap = 10, showStepLabels = true, options } = props;
|
|
24
25
|
const { title, desc, items = [] } = data;
|
|
25
26
|
|
|
26
27
|
const titleContent = Title ? <Title title={title} desc={desc} /> : null;
|
|
@@ -90,20 +91,22 @@ export const SequenceTimeline: ComponentType<SequenceTimelineProps> = (
|
|
|
90
91
|
const nodeY = itemY + itemBounds.height / 2;
|
|
91
92
|
const indexes = [index];
|
|
92
93
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
94
|
+
if (showStepLabels) {
|
|
95
|
+
decorElements.push(
|
|
96
|
+
<Text
|
|
97
|
+
x={stepLabelX}
|
|
98
|
+
y={nodeY}
|
|
99
|
+
width={70}
|
|
100
|
+
fontSize={18}
|
|
101
|
+
fontWeight="bold"
|
|
102
|
+
alignHorizontal="left"
|
|
103
|
+
alignVertical="middle"
|
|
104
|
+
fill={palette[index % palette.length]}
|
|
105
|
+
>
|
|
106
|
+
{`STEP ${index + 1}`}
|
|
107
|
+
</Text>,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
107
110
|
|
|
108
111
|
itemElements.push(
|
|
109
112
|
<Item
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
import type { JSXElement } from '../../jsx';
|
|
2
|
+
import { Path, Polygon } from '../../jsx';
|
|
3
|
+
|
|
4
|
+
export const getMidPoint = (points: [number, number][]) => {
|
|
5
|
+
if (points.length === 0) return null;
|
|
6
|
+
if (points.length === 1) return points[0];
|
|
7
|
+
let total = 0;
|
|
8
|
+
const segments: {
|
|
9
|
+
length: number;
|
|
10
|
+
start: [number, number];
|
|
11
|
+
end: [number, number];
|
|
12
|
+
}[] = [];
|
|
13
|
+
for (let i = 0; i < points.length - 1; i += 1) {
|
|
14
|
+
const start = points[i];
|
|
15
|
+
const end = points[i + 1];
|
|
16
|
+
const length = Math.hypot(end[0] - start[0], end[1] - start[1]);
|
|
17
|
+
segments.push({ length, start, end });
|
|
18
|
+
total += length;
|
|
19
|
+
}
|
|
20
|
+
if (total === 0) return points[0];
|
|
21
|
+
let target = total / 2;
|
|
22
|
+
for (let i = 0; i < segments.length; i += 1) {
|
|
23
|
+
const segment = segments[i];
|
|
24
|
+
if (target <= segment.length || i === segments.length - 1) {
|
|
25
|
+
const ratio =
|
|
26
|
+
segment.length === 0
|
|
27
|
+
? 0
|
|
28
|
+
: Math.max(0, Math.min(1, target / segment.length));
|
|
29
|
+
return [
|
|
30
|
+
segment.start[0] + (segment.end[0] - segment.start[0]) * ratio,
|
|
31
|
+
segment.start[1] + (segment.end[1] - segment.start[1]) * ratio,
|
|
32
|
+
] as [number, number];
|
|
33
|
+
}
|
|
34
|
+
target -= segment.length;
|
|
35
|
+
}
|
|
36
|
+
return points[Math.floor(points.length / 2)];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const createStraightPath = (
|
|
40
|
+
points: [number, number][],
|
|
41
|
+
dx: number,
|
|
42
|
+
dy: number,
|
|
43
|
+
) =>
|
|
44
|
+
points
|
|
45
|
+
.map(([x, y], index) => {
|
|
46
|
+
const prefix = index === 0 ? 'M' : 'L';
|
|
47
|
+
return `${prefix} ${x + dx} ${y + dy}`;
|
|
48
|
+
})
|
|
49
|
+
.join(' ');
|
|
50
|
+
|
|
51
|
+
export const createRoundedPath = (
|
|
52
|
+
points: [number, number][],
|
|
53
|
+
radius: number,
|
|
54
|
+
dx: number,
|
|
55
|
+
dy: number,
|
|
56
|
+
) => {
|
|
57
|
+
if (points.length < 2) return '';
|
|
58
|
+
const clamp = (value: number, min: number, max: number) =>
|
|
59
|
+
Math.min(max, Math.max(min, value));
|
|
60
|
+
const toPoint = ([x, y]: [number, number]) => ({
|
|
61
|
+
x: x + dx,
|
|
62
|
+
y: y + dy,
|
|
63
|
+
});
|
|
64
|
+
const output: string[] = [];
|
|
65
|
+
const first = toPoint(points[0]);
|
|
66
|
+
output.push(`M ${first.x} ${first.y}`);
|
|
67
|
+
|
|
68
|
+
if (points.length === 2) {
|
|
69
|
+
const last = toPoint(points[1]);
|
|
70
|
+
output.push(`L ${last.x} ${last.y}`);
|
|
71
|
+
return output.join(' ');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
for (let i = 1; i < points.length - 1; i += 1) {
|
|
75
|
+
const prev = points[i - 1];
|
|
76
|
+
const curr = points[i];
|
|
77
|
+
const next = points[i + 1];
|
|
78
|
+
const v0x = curr[0] - prev[0];
|
|
79
|
+
const v0y = curr[1] - prev[1];
|
|
80
|
+
const v1x = next[0] - curr[0];
|
|
81
|
+
const v1y = next[1] - curr[1];
|
|
82
|
+
const d0 = Math.hypot(v0x, v0y);
|
|
83
|
+
const d1 = Math.hypot(v1x, v1y);
|
|
84
|
+
if (d0 === 0 || d1 === 0) {
|
|
85
|
+
const currPoint = toPoint(curr);
|
|
86
|
+
output.push(`L ${currPoint.x} ${currPoint.y}`);
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
const r = clamp(radius, 0, Math.min(d0, d1) / 2);
|
|
90
|
+
if (r === 0) {
|
|
91
|
+
const currPoint = toPoint(curr);
|
|
92
|
+
output.push(`L ${currPoint.x} ${currPoint.y}`);
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
const u0x = v0x / d0;
|
|
96
|
+
const u0y = v0y / d0;
|
|
97
|
+
const u1x = v1x / d1;
|
|
98
|
+
const u1y = v1y / d1;
|
|
99
|
+
const start = toPoint([curr[0] - u0x * r, curr[1] - u0y * r]);
|
|
100
|
+
const end = toPoint([curr[0] + u1x * r, curr[1] + u1y * r]);
|
|
101
|
+
output.push(`L ${start.x} ${start.y}`);
|
|
102
|
+
const currPoint = toPoint(curr);
|
|
103
|
+
output.push(`Q ${currPoint.x} ${currPoint.y} ${end.x} ${end.y}`);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const last = toPoint(points[points.length - 1]);
|
|
107
|
+
output.push(`L ${last.x} ${last.y}`);
|
|
108
|
+
return output.join(' ');
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export const createArrowElements = (
|
|
112
|
+
x: number,
|
|
113
|
+
y: number,
|
|
114
|
+
angle: number,
|
|
115
|
+
type: 'arrow' | 'triangle' | 'diamond',
|
|
116
|
+
fillColor: string,
|
|
117
|
+
edgeWidth: number,
|
|
118
|
+
arrowSize: number,
|
|
119
|
+
): JSXElement[] => {
|
|
120
|
+
const ux = Math.cos(angle);
|
|
121
|
+
const uy = Math.sin(angle);
|
|
122
|
+
const px = -uy;
|
|
123
|
+
const py = ux;
|
|
124
|
+
const length = arrowSize;
|
|
125
|
+
const halfWidth = arrowSize * 0.55;
|
|
126
|
+
|
|
127
|
+
if (type === 'arrow') {
|
|
128
|
+
const leftX = x - ux * length + px * halfWidth;
|
|
129
|
+
const leftY = y - uy * length + py * halfWidth;
|
|
130
|
+
const rightX = x - ux * length - px * halfWidth;
|
|
131
|
+
const rightY = y - uy * length - py * halfWidth;
|
|
132
|
+
return [
|
|
133
|
+
<Path
|
|
134
|
+
d={`M ${leftX} ${leftY} L ${x} ${y} L ${rightX} ${rightY}`}
|
|
135
|
+
stroke={fillColor}
|
|
136
|
+
strokeWidth={Math.max(1.5, edgeWidth)}
|
|
137
|
+
strokeLinecap="round"
|
|
138
|
+
strokeLinejoin="round"
|
|
139
|
+
fill="none"
|
|
140
|
+
/>,
|
|
141
|
+
];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (type === 'diamond') {
|
|
145
|
+
const diamondLength = length * 1.25;
|
|
146
|
+
const diamondWidth = halfWidth * 0.75;
|
|
147
|
+
const midX = x - ux * diamondLength * 0.5;
|
|
148
|
+
const midY = y - uy * diamondLength * 0.5;
|
|
149
|
+
const diamondPoints = [
|
|
150
|
+
{ x, y },
|
|
151
|
+
{ x: midX + px * diamondWidth, y: midY + py * diamondWidth },
|
|
152
|
+
{ x: x - ux * diamondLength, y: y - uy * diamondLength },
|
|
153
|
+
{ x: midX - px * diamondWidth, y: midY - py * diamondWidth },
|
|
154
|
+
];
|
|
155
|
+
return [
|
|
156
|
+
<Polygon
|
|
157
|
+
points={diamondPoints}
|
|
158
|
+
fill={fillColor}
|
|
159
|
+
stroke={fillColor}
|
|
160
|
+
strokeWidth={Math.max(1, edgeWidth * 0.8)}
|
|
161
|
+
/>,
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const trianglePoints = [
|
|
166
|
+
{ x, y },
|
|
167
|
+
{
|
|
168
|
+
x: x - ux * length + px * halfWidth,
|
|
169
|
+
y: y - uy * length + py * halfWidth,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
x: x - ux * length - px * halfWidth,
|
|
173
|
+
y: y - uy * length - py * halfWidth,
|
|
174
|
+
},
|
|
175
|
+
];
|
|
176
|
+
return [
|
|
177
|
+
<Polygon
|
|
178
|
+
points={trianglePoints}
|
|
179
|
+
fill={fillColor}
|
|
180
|
+
stroke={fillColor}
|
|
181
|
+
strokeWidth={Math.max(1, edgeWidth * 0.8)}
|
|
182
|
+
/>,
|
|
183
|
+
];
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
// LT: Left Top (radio), LC: Left Center (1/2), LB: Left Bottom (1 - radio)
|
|
187
|
+
// RT: Right Top (radio), RC: Right Center (1/2), RB: Right Bottom (1 - radio)
|
|
188
|
+
export const getNodesAnchors = (node: {
|
|
189
|
+
x: number;
|
|
190
|
+
y: number;
|
|
191
|
+
width: number;
|
|
192
|
+
height: number;
|
|
193
|
+
radio?: number;
|
|
194
|
+
}) => {
|
|
195
|
+
const { x, y, width, height, radio = 0.25 } = node;
|
|
196
|
+
const q1H = height * radio;
|
|
197
|
+
const halfH = height * 0.5;
|
|
198
|
+
const q3H = height * (1 - radio);
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
LT: { x, y: y + q1H },
|
|
202
|
+
LC: { x, y: y + halfH },
|
|
203
|
+
LB: { x, y: y + q3H },
|
|
204
|
+
RT: { x: x + width, y: y + q1H },
|
|
205
|
+
RC: { x: x + width, y: y + halfH },
|
|
206
|
+
RB: { x: x + width, y: y + q3H },
|
|
207
|
+
};
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
export const getTangentAngle = (points: [number, number][], t: 0 | 1) => {
|
|
211
|
+
const len = points.length;
|
|
212
|
+
// Cubic Bezier (Self loop)
|
|
213
|
+
if (len === 4) {
|
|
214
|
+
const p0 = points[0],
|
|
215
|
+
p1 = points[1],
|
|
216
|
+
p2 = points[2],
|
|
217
|
+
p3 = points[3];
|
|
218
|
+
if (t === 0) {
|
|
219
|
+
return Math.atan2(p1[1] - p0[1], p1[0] - p0[0]);
|
|
220
|
+
} else {
|
|
221
|
+
return Math.atan2(p3[1] - p2[1], p3[0] - p2[0]);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
// Quad Bezier (Curved)
|
|
225
|
+
if (len === 3) {
|
|
226
|
+
const p0 = points[0],
|
|
227
|
+
p1 = points[1],
|
|
228
|
+
p2 = points[2];
|
|
229
|
+
if (t === 0) {
|
|
230
|
+
return Math.atan2(p1[1] - p0[1], p1[0] - p0[0]);
|
|
231
|
+
} else {
|
|
232
|
+
return Math.atan2(p2[1] - p1[1], p2[0] - p1[0]);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Line
|
|
236
|
+
if (len === 2) {
|
|
237
|
+
const p0 = points[0],
|
|
238
|
+
p1 = points[1];
|
|
239
|
+
const angle = Math.atan2(p1[1] - p0[1], p1[0] - p0[0]);
|
|
240
|
+
return angle;
|
|
241
|
+
}
|
|
242
|
+
return 0;
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* 计算贝塞尔曲线上任意 t (0-1) 位置的点
|
|
247
|
+
*/
|
|
248
|
+
export const getPointAtT = (
|
|
249
|
+
points: [number, number][],
|
|
250
|
+
t = 0.5,
|
|
251
|
+
): [number, number] => {
|
|
252
|
+
const len = points.length;
|
|
253
|
+
if (len === 4) {
|
|
254
|
+
const [p0, p1, p2, p3] = points;
|
|
255
|
+
const mt = 1 - t;
|
|
256
|
+
// B(t) = (1-t)^3*P0 + 3(1-t)^2*t*P1 + 3(1-t)*t^2*P2 + t^3*P3
|
|
257
|
+
return [
|
|
258
|
+
mt ** 3 * p0[0] +
|
|
259
|
+
3 * mt ** 2 * t * p1[0] +
|
|
260
|
+
3 * mt * t ** 2 * p2[0] +
|
|
261
|
+
t ** 3 * p3[0],
|
|
262
|
+
mt ** 3 * p0[1] +
|
|
263
|
+
3 * mt ** 2 * t * p1[1] +
|
|
264
|
+
3 * mt * t ** 2 * p2[1] +
|
|
265
|
+
t ** 3 * p3[1],
|
|
266
|
+
];
|
|
267
|
+
}
|
|
268
|
+
if (len === 3) {
|
|
269
|
+
const [p0, p1, p2] = points;
|
|
270
|
+
const mt = 1 - t;
|
|
271
|
+
// B(t) = (1-t)^2*P0 + 2(1-t)*t*P1 + t^2*P2
|
|
272
|
+
return [
|
|
273
|
+
mt ** 2 * p0[0] + 2 * mt * t * p1[0] + t ** 2 * p2[0],
|
|
274
|
+
mt ** 2 * p0[1] + 2 * mt * t * p1[1] + t ** 2 * p2[1],
|
|
275
|
+
];
|
|
276
|
+
}
|
|
277
|
+
if (len === 2) {
|
|
278
|
+
const [p0, p1] = points;
|
|
279
|
+
return [p0[0] + (p1[0] - p0[0]) * t, p0[1] + (p1[1] - p0[1]) * t];
|
|
280
|
+
}
|
|
281
|
+
return points[0] || [0, 0];
|
|
282
|
+
};
|
|
283
|
+
|
|
284
|
+
export const getLabelPosition = (
|
|
285
|
+
points: [number, number][],
|
|
286
|
+
selfLoopOffset = 10,
|
|
287
|
+
) => {
|
|
288
|
+
const len = points.length;
|
|
289
|
+
// 默认取中点
|
|
290
|
+
const labelPoint = getPointAtT(points);
|
|
291
|
+
|
|
292
|
+
if (len === 4) {
|
|
293
|
+
// 针对自连接(len=4)的特殊偏移处理
|
|
294
|
+
labelPoint[0] += selfLoopOffset;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return labelPoint as [number, number];
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
export const getEdgePathD = (points: [number, number][]) => {
|
|
301
|
+
const len = points.length;
|
|
302
|
+
if (len === 4) {
|
|
303
|
+
const [p0, p1, p2, p3] = points;
|
|
304
|
+
return `M ${p0[0]} ${p0[1]} C ${p1[0]} ${p1[1]} ${p2[0]} ${p2[1]} ${p3[0]} ${p3[1]}`;
|
|
305
|
+
}
|
|
306
|
+
if (len === 3) {
|
|
307
|
+
const [p0, p1, p2] = points;
|
|
308
|
+
return `M ${p0[0]} ${p0[1]} Q ${p1[0]} ${p1[1]} ${p2[0]} ${p2[1]}`;
|
|
309
|
+
}
|
|
310
|
+
if (len === 2) {
|
|
311
|
+
const [p0, p1] = points;
|
|
312
|
+
return `M ${p0[0]} ${p0[1]} L ${p1[0]} ${p1[1]}`;
|
|
313
|
+
}
|
|
314
|
+
return '';
|
|
315
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { get } from 'lodash-es';
|
|
2
|
+
import type { InfographicOptionPath } from '../../options';
|
|
2
3
|
import { ISyncRegistry, SyncHandler } from '../types';
|
|
3
4
|
|
|
4
5
|
type OptionsGetter = () => any;
|
|
@@ -12,7 +13,7 @@ export class SyncRegistry implements ISyncRegistry {
|
|
|
12
13
|
constructor(private getOptions: OptionsGetter) {}
|
|
13
14
|
|
|
14
15
|
register(
|
|
15
|
-
path: string,
|
|
16
|
+
path: InfographicOptionPath | (string & {}),
|
|
16
17
|
handler: SyncHandler,
|
|
17
18
|
options?: { immediate?: boolean },
|
|
18
19
|
): () => void {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { InfographicOptionPath } from '../../options';
|
|
1
2
|
import type { ICommandManager } from './command';
|
|
2
3
|
import type { IInteractionManager } from './interaction';
|
|
3
4
|
import type { IPluginManager } from './plugin';
|
|
@@ -13,7 +14,7 @@ export interface IEditor {
|
|
|
13
14
|
|
|
14
15
|
getDocument(): SVGSVGElement;
|
|
15
16
|
registerSync(
|
|
16
|
-
path: string,
|
|
17
|
+
path: InfographicOptionPath | (string & {}),
|
|
17
18
|
handler: SyncHandler,
|
|
18
19
|
options?: { immediate?: boolean },
|
|
19
20
|
): () => void;
|
package/src/editor/types/sync.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { InfographicOptionPath } from '../../options';
|
|
2
|
+
|
|
1
3
|
/**
|
|
2
4
|
* Sync callback
|
|
3
5
|
* @param newValue The new value after modification
|
|
@@ -14,7 +16,7 @@ export interface ISyncRegistry {
|
|
|
14
16
|
* @returns unregister function
|
|
15
17
|
*/
|
|
16
18
|
register(
|
|
17
|
-
path: string,
|
|
19
|
+
path: InfographicOptionPath | (string & {}),
|
|
18
20
|
handler: SyncHandler,
|
|
19
21
|
options?: { immediate?: boolean },
|
|
20
22
|
): () => void;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cloneDeep,
|
|
1
|
+
import { cloneDeep, isEqual, isPlainObject } from 'lodash-es';
|
|
2
2
|
|
|
3
3
|
export interface ApplyOptionUpdatesOptions {
|
|
4
4
|
/** Whether to notify parent paths of changes (bubbling) */
|
|
@@ -22,85 +22,95 @@ export function applyOptionUpdates(
|
|
|
22
22
|
options?: ApplyOptionUpdatesOptions,
|
|
23
23
|
): void {
|
|
24
24
|
const { bubbleUp = false, collector } = options ?? {};
|
|
25
|
-
// Set to store unique parent paths that need notification
|
|
26
|
-
const parentPathsToNotify = new Set<string>();
|
|
27
25
|
|
|
28
|
-
applyOptionUpdatesInternal(
|
|
26
|
+
const hasChange = applyOptionUpdatesInternal(
|
|
29
27
|
target,
|
|
30
28
|
source,
|
|
31
29
|
basePath,
|
|
32
30
|
collector,
|
|
33
31
|
bubbleUp,
|
|
34
|
-
parentPathsToNotify,
|
|
35
32
|
);
|
|
36
33
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// Sort by path depth in descending order (deepest first)
|
|
40
|
-
const sortedPaths = Array.from(parentPathsToNotify).sort((a, b) => {
|
|
41
|
-
const depthA = a === '' ? 0 : a.split('.').length;
|
|
42
|
-
const depthB = b === '' ? 0 : b.split('.').length;
|
|
43
|
-
return depthB - depthA;
|
|
44
|
-
});
|
|
45
|
-
for (const parentPath of sortedPaths) {
|
|
46
|
-
const newVal = parentPath ? get(target, parentPath) : target;
|
|
47
|
-
// For parent paths, we provide the cloned new value.
|
|
48
|
-
// oldVal is passed as undefined as tracking branch node state is complex.
|
|
49
|
-
collector(parentPath, cloneDeep(newVal), undefined);
|
|
50
|
-
}
|
|
34
|
+
if (basePath === '' && hasChange && bubbleUp && collector) {
|
|
35
|
+
collector('', cloneDeep(target), undefined);
|
|
51
36
|
}
|
|
52
37
|
}
|
|
53
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Internal recursive function.
|
|
41
|
+
* Returns true if any change occurred within this branch (or its children).
|
|
42
|
+
*/
|
|
54
43
|
function applyOptionUpdatesInternal(
|
|
55
44
|
target: any,
|
|
56
45
|
source: any,
|
|
57
46
|
basePath: string,
|
|
58
47
|
collector: ((path: string, newVal: any, oldVal: any) => void) | undefined,
|
|
59
48
|
bubbleUp: boolean,
|
|
60
|
-
|
|
61
|
-
|
|
49
|
+
): boolean {
|
|
50
|
+
let hasChange = false;
|
|
51
|
+
|
|
62
52
|
Object.keys(source).forEach((key) => {
|
|
53
|
+
if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
63
57
|
const fullPath = basePath ? `${basePath}.${key}` : key;
|
|
64
58
|
const updateValue = source[key];
|
|
65
59
|
const oldValue = target[key];
|
|
60
|
+
let childChanged = false;
|
|
66
61
|
|
|
67
62
|
if (updateValue === undefined) {
|
|
68
|
-
delete
|
|
69
|
-
|
|
70
|
-
|
|
63
|
+
// Handle deletion: Only delete and notify if the key actually exists
|
|
64
|
+
if (key in target) {
|
|
65
|
+
delete target[key];
|
|
66
|
+
collector?.(fullPath, undefined, oldValue);
|
|
67
|
+
childChanged = true;
|
|
68
|
+
}
|
|
71
69
|
} else if (isPlainObject(updateValue)) {
|
|
72
|
-
|
|
70
|
+
// Handle nested object
|
|
71
|
+
const oldValueIsObject = isPlainObject(target[key]);
|
|
72
|
+
if (!oldValueIsObject) {
|
|
73
73
|
target[key] = {};
|
|
74
74
|
}
|
|
75
|
-
|
|
75
|
+
|
|
76
|
+
const grandChildChanged = applyOptionUpdatesInternal(
|
|
76
77
|
target[key],
|
|
77
78
|
updateValue,
|
|
78
79
|
fullPath,
|
|
79
80
|
collector,
|
|
80
81
|
bubbleUp,
|
|
81
|
-
parentPathsToNotify,
|
|
82
82
|
);
|
|
83
|
+
|
|
84
|
+
if (!oldValueIsObject) {
|
|
85
|
+
// Overwriting a primitive with an object is always a change.
|
|
86
|
+
childChanged = true;
|
|
87
|
+
// If the object was empty (grandChildChanged is false), we still need to report it.
|
|
88
|
+
if (!grandChildChanged) {
|
|
89
|
+
collector?.(fullPath, target[key], oldValue);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
childChanged = grandChildChanged;
|
|
93
|
+
}
|
|
83
94
|
} else {
|
|
95
|
+
// Handle primitive update
|
|
84
96
|
target[key] = updateValue;
|
|
85
97
|
if (!isEqual(updateValue, oldValue)) {
|
|
86
98
|
collector?.(fullPath, updateValue, oldValue);
|
|
87
|
-
|
|
99
|
+
childChanged = true;
|
|
88
100
|
}
|
|
89
101
|
}
|
|
102
|
+
|
|
103
|
+
if (childChanged) {
|
|
104
|
+
hasChange = true;
|
|
105
|
+
}
|
|
90
106
|
});
|
|
91
|
-
}
|
|
92
107
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
set.add(''); // Root path
|
|
99
|
-
return;
|
|
108
|
+
// Bubbling: Notify if any child changed in this branch
|
|
109
|
+
// The recursion naturally ensures this happens in "deepest-first" (post-order) sequence.
|
|
110
|
+
if (hasChange && bubbleUp && basePath !== '') {
|
|
111
|
+
// Current target is now fully updated for this scope.
|
|
112
|
+
collector?.(basePath, cloneDeep(target), undefined);
|
|
100
113
|
}
|
|
101
114
|
|
|
102
|
-
|
|
103
|
-
for (let i = parts.length; i >= 0; i--) {
|
|
104
|
-
set.add(parts.slice(0, i).join('.'));
|
|
105
|
-
}
|
|
115
|
+
return hasChange;
|
|
106
116
|
}
|
package/src/exporter/png.ts
CHANGED
|
@@ -6,8 +6,9 @@ export async function exportToPNGString(
|
|
|
6
6
|
svg: SVGSVGElement,
|
|
7
7
|
options: Omit<PNGExportOptions, 'type'> = {},
|
|
8
8
|
): Promise<string> {
|
|
9
|
-
const { dpr = globalThis.devicePixelRatio ?? 2 } =
|
|
10
|
-
|
|
9
|
+
const { dpr = globalThis.devicePixelRatio ?? 2, removeBackground = false } =
|
|
10
|
+
options;
|
|
11
|
+
const node = await exportToSVG(svg, { removeBackground });
|
|
11
12
|
|
|
12
13
|
const { width, height } = getViewBox(node);
|
|
13
14
|
|
package/src/exporter/svg.ts
CHANGED
|
@@ -23,7 +23,11 @@ export async function exportToSVG(
|
|
|
23
23
|
svg: SVGSVGElement,
|
|
24
24
|
options: Omit<SVGExportOptions, 'type'> = {},
|
|
25
25
|
) {
|
|
26
|
-
const {
|
|
26
|
+
const {
|
|
27
|
+
removeBackground = false,
|
|
28
|
+
embedResources = true,
|
|
29
|
+
removeIds = false,
|
|
30
|
+
} = options;
|
|
27
31
|
const clonedSVG = svg.cloneNode(true) as SVGSVGElement;
|
|
28
32
|
const { width, height } = getViewBox(svg);
|
|
29
33
|
setAttributes(clonedSVG, { width, height });
|
|
@@ -36,6 +40,9 @@ export async function exportToSVG(
|
|
|
36
40
|
}
|
|
37
41
|
await embedFonts(clonedSVG, embedResources);
|
|
38
42
|
|
|
43
|
+
if (removeBackground) {
|
|
44
|
+
removeSVGBackground(clonedSVG);
|
|
45
|
+
}
|
|
39
46
|
cleanSVG(clonedSVG);
|
|
40
47
|
|
|
41
48
|
return clonedSVG;
|
|
@@ -322,6 +329,12 @@ function cleanSVG(svg: SVGSVGElement) {
|
|
|
322
329
|
clearDataset(svg);
|
|
323
330
|
}
|
|
324
331
|
|
|
332
|
+
function removeSVGBackground(svg: SVGSVGElement) {
|
|
333
|
+
svg.style.removeProperty('background-color');
|
|
334
|
+
const background = getElementByRole(svg, ElementTypeEnum.Background);
|
|
335
|
+
background?.remove();
|
|
336
|
+
}
|
|
337
|
+
|
|
325
338
|
function removeBtnGroup(svg: SVGSVGElement) {
|
|
326
339
|
const btnGroup = getElementByRole(svg, ElementTypeEnum.BtnsGroup);
|
|
327
340
|
btnGroup?.remove();
|
package/src/exporter/types.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
|
|
@@ -14,6 +19,11 @@ export interface SVGExportOptions {
|
|
|
14
19
|
|
|
15
20
|
export interface PNGExportOptions {
|
|
16
21
|
type: 'png';
|
|
22
|
+
/**
|
|
23
|
+
* 是否移除背景(SVG 背景样式 + 背景矩形)
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
26
|
+
removeBackground?: boolean;
|
|
17
27
|
/**
|
|
18
28
|
* 设备像素比,默认为浏览器的 devicePixelRatio
|
|
19
29
|
* @default globalThis.devicePixelRatio || 2
|
package/src/options/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { DesignOptions, ParsedDesignsOptions } from '../designs';
|
|
|
2
2
|
import type { ElementProps, IInteraction, IPlugin } from '../editor';
|
|
3
3
|
import type { ThemeConfig } from '../themes';
|
|
4
4
|
import type { Data, Padding, ParsedData } from '../types';
|
|
5
|
+
import type { Path } from '../utils';
|
|
5
6
|
|
|
6
7
|
export interface InfographicOptions {
|
|
7
8
|
/** 容器,可以是选择器或者 HTMLElement */
|
|
@@ -66,3 +67,9 @@ interface SVGOptions {
|
|
|
66
67
|
/** 是否启用背景 */
|
|
67
68
|
background?: boolean;
|
|
68
69
|
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* All valid property paths for Infographic Options.
|
|
73
|
+
* Use this to validate paths in SyncRegistry and other places.
|
|
74
|
+
*/
|
|
75
|
+
export type InfographicOptionPath = Path<UpdatableInfographicOptions>;
|