@zhangqingcq/vgce 0.1.35 → 0.2.0
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/dist/style.css +1 -1
- package/dist/vgce.js +4546 -4232
- package/dist/vgce.umd.cjs +25 -22
- package/package.json +1 -1
- package/src/assets/icons/svg-file.svg +1 -0
- package/src/components/svg-editor/center-panel.vue +31 -5
- package/src/components/svg-editor/connection-line.vue +4 -2
- package/src/components/svg-editor/index.vue +26 -1
- package/src/components/svg-editor/top-panel.vue +5 -2
- package/src/utils/export-svg.ts +423 -0
package/package.json
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1793173027617" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3032" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M219.424 18.304h530.272l201.152 219.424v768H219.424V18.304z" fill="none" p-id="3033"></path><path d="M733.696 253.728V50.304H251.424v923.424h667.424v-720h-185.152z m217.152-16v768H219.424V18.304h530.272l201.152 219.424z m-58.08-16l-127.04-138.624v138.624h127.04z" class="icon-out" p-id="3034" data-spm-anchor-id="a313x.search_index.0.i4.44c03a81Y6jSmC"></path><path d="M73.152 288a32 32 0 0 1 32-32h448a32 32 0 0 1 32 32v448a32 32 0 0 1-32 32h-448a32 32 0 0 1-32-32V288zM640 416h224v37.344h-224V416zM640 509.344h224v37.312h-224v-37.312zM640 602.656h224V640h-224v-37.344z" class="icon-in" p-id="3035" data-spm-anchor-id="a313x.search_index.0.i6.44c03a81Y6jSmC"></path><path d="M224 574.112l54.016-5.248c3.232 18.144 9.792 31.456 19.68 39.936 9.984 8.512 23.424 12.768 40.32 12.768 17.856 0 31.296-3.744 40.32-11.264 9.088-7.616 13.664-16.48 13.664-26.624a24.416 24.416 0 0 0-5.824-16.48c-3.744-4.64-10.368-8.64-19.84-12-6.528-2.24-21.344-6.272-44.48-12-29.76-7.36-50.592-16.448-62.592-27.2-16.896-15.136-25.312-33.568-25.312-55.328 0-13.984 3.936-27.04 11.808-39.168 8-12.256 19.424-21.568 34.304-27.936 15.008-6.4 33.056-9.568 54.176-9.568 34.496 0 60.448 7.552 77.824 22.688 17.504 15.136 26.688 35.328 27.552 60.576l-55.488 2.432c-2.368-14.144-7.488-24.256-15.36-30.4-7.776-6.24-19.456-9.376-35.072-9.376-16.128 0-28.768 3.328-37.888 9.952a20.032 20.032 0 0 0-8.8 17.056 21.12 21.12 0 0 0 8.256 16.704c6.976 5.856 24 12 50.976 18.368 27.008 6.4 46.944 12.992 59.84 19.872 12.992 6.752 23.104 16.064 30.368 27.936 7.36 11.744 11.04 26.304 11.04 43.68 0 15.776-4.352 30.496-13.12 44.256-8.736 13.76-21.12 24-37.12 30.752-16 6.624-35.936 9.92-59.84 9.92-34.72 0-61.408-8-80.032-24-18.624-16.096-29.76-39.552-33.376-70.304z" fill="#FFFFFF" p-id="3036"></path></svg>
|
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
import type { IVisibleInfo } from '../config'
|
|
39
39
|
import { useContextMenuStore, useEditPrivateStore } from '@/stores/system'
|
|
40
40
|
import { EContextMenuInfoType } from '@/stores/system/types'
|
|
41
|
+
import { exportCanvasSvg } from '@/utils/export-svg'
|
|
41
42
|
|
|
42
43
|
const globalStore = useGlobalStore(pinia)
|
|
43
44
|
const configStore = useConfigStore(pinia)
|
|
@@ -50,6 +51,7 @@
|
|
|
50
51
|
componentsRegister(props.vueComp)
|
|
51
52
|
const contextMenuRef = ref<HTMLElement>()
|
|
52
53
|
const canvasRef = ref<HTMLElement>()
|
|
54
|
+
const svgRef = ref<SVGSVGElement>()
|
|
53
55
|
const ct: Record<string, any> = {
|
|
54
56
|
MoveCanvas: 'grab',
|
|
55
57
|
Rotate: "url('/src/assets/icons/rotate.svg'), auto",
|
|
@@ -1002,7 +1004,18 @@
|
|
|
1002
1004
|
})
|
|
1003
1005
|
})
|
|
1004
1006
|
|
|
1005
|
-
|
|
1007
|
+
const exportSvg = (filename?: string) => {
|
|
1008
|
+
if (!svgRef.value) {
|
|
1009
|
+
return false
|
|
1010
|
+
}
|
|
1011
|
+
exportCanvasSvg(svgRef.value, {
|
|
1012
|
+
filename,
|
|
1013
|
+
background: configStore.svg.background_color
|
|
1014
|
+
})
|
|
1015
|
+
return true
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
defineExpose({ onCanvasMouseUp, exportSvg })
|
|
1006
1019
|
</script>
|
|
1007
1020
|
|
|
1008
1021
|
<template>
|
|
@@ -1024,17 +1037,26 @@
|
|
|
1024
1037
|
>
|
|
1025
1038
|
<slot name="background" />
|
|
1026
1039
|
<div class="coverLayer">
|
|
1027
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
|
|
1040
|
+
<svg ref="svgRef" xmlns="http://www.w3.org/2000/svg" width="100%" height="100%">
|
|
1028
1041
|
<defs>
|
|
1029
|
-
<pattern
|
|
1042
|
+
<pattern
|
|
1043
|
+
id="pattern_grid"
|
|
1044
|
+
data-editor-only
|
|
1045
|
+
patternUnits="userSpaceOnUse"
|
|
1046
|
+
x="0"
|
|
1047
|
+
y="0"
|
|
1048
|
+
width="20"
|
|
1049
|
+
height="20"
|
|
1050
|
+
>
|
|
1030
1051
|
<line x1="0" y1="0" x2="0" y2="20" :stroke="configStore.svg.grid_color" />
|
|
1031
1052
|
<line x1="10" y1="0" x2="10" y2="20" :stroke="configStore.svg.grid_color" />
|
|
1032
1053
|
<line x1="0" y1="0" x2="20" y2="0" :stroke="configStore.svg.grid_color" />
|
|
1033
1054
|
<line x1="0" y1="10" x2="20" y2="10" :stroke="configStore.svg.grid_color" />
|
|
1034
1055
|
</pattern>
|
|
1035
1056
|
</defs>
|
|
1036
|
-
<rect v-if="configStore.svg.grid" width="100%" height="100%" fill="url(#pattern_grid)" />
|
|
1057
|
+
<rect v-if="configStore.svg.grid" data-editor-only width="100%" height="100%" fill="url(#pattern_grid)" />
|
|
1037
1058
|
<g
|
|
1059
|
+
data-canvas-content
|
|
1038
1060
|
:transform="`translate(${svgEditLayoutStore.center_offset.x * configStore.svg.scale},${
|
|
1039
1061
|
svgEditLayoutStore.center_offset.y * configStore.svg.scale
|
|
1040
1062
|
})rotate(${0})scale(${configStore.svg.scale})`"
|
|
@@ -1126,6 +1148,7 @@
|
|
|
1126
1148
|
/>
|
|
1127
1149
|
<rect
|
|
1128
1150
|
v-if="item.config.actual_rect"
|
|
1151
|
+
data-editor-only
|
|
1129
1152
|
:id="`rect${item.id}`"
|
|
1130
1153
|
fill="black"
|
|
1131
1154
|
fill-opacity="0"
|
|
@@ -1152,10 +1175,12 @@
|
|
|
1152
1175
|
visible_info.handle_panel &&
|
|
1153
1176
|
item.type !== EDoneJsonType.ConnectionLine
|
|
1154
1177
|
"
|
|
1178
|
+
data-editor-only
|
|
1155
1179
|
:item-info="item"
|
|
1156
1180
|
/>
|
|
1157
1181
|
<connection-panel
|
|
1158
1182
|
@contextmenu="onCanvasContextMenuEvent"
|
|
1183
|
+
data-editor-only
|
|
1159
1184
|
v-if="
|
|
1160
1185
|
!item.selected &&
|
|
1161
1186
|
visible_info.select_item.info?.id == item.id &&
|
|
@@ -1173,6 +1198,7 @@
|
|
|
1173
1198
|
</g>
|
|
1174
1199
|
<!--框选-->
|
|
1175
1200
|
<rect
|
|
1201
|
+
data-editor-only
|
|
1176
1202
|
:x="selectRect.x"
|
|
1177
1203
|
:y="selectRect.y"
|
|
1178
1204
|
:height="selectRect.height"
|
|
@@ -1294,4 +1320,4 @@
|
|
|
1294
1320
|
.common-ani {
|
|
1295
1321
|
transform-box: fill-box;
|
|
1296
1322
|
}
|
|
1297
|
-
</style>
|
|
1323
|
+
</style>
|
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
<g :style="{ cursor: globalStore.intention === EGlobalStoreIntention.Connection ? 'crosshair' : 'move' }">
|
|
44
44
|
<!-- 选中效果 -->
|
|
45
45
|
<path
|
|
46
|
+
data-editor-only
|
|
46
47
|
:d="positionArrToPath(props.itemInfo.props.point_position.val)"
|
|
47
48
|
fill="none"
|
|
48
49
|
fill-opacity="0"
|
|
@@ -54,6 +55,7 @@
|
|
|
54
55
|
/>
|
|
55
56
|
<!-- 打底 -->
|
|
56
57
|
<path
|
|
58
|
+
data-editor-only
|
|
57
59
|
:d="positionArrToPath(props.itemInfo.props.point_position.val)"
|
|
58
60
|
fill="none"
|
|
59
61
|
fill-opacity="0"
|
|
@@ -133,7 +135,7 @@
|
|
|
133
135
|
>
|
|
134
136
|
</animateMotion>
|
|
135
137
|
</circle>
|
|
136
|
-
<g>
|
|
138
|
+
<g data-editor-only>
|
|
137
139
|
<!-- 节点选中效果 -->
|
|
138
140
|
<circle
|
|
139
141
|
v-for="(item, index) in props.itemInfo.props.point_position.val"
|
|
@@ -162,4 +164,4 @@
|
|
|
162
164
|
/>
|
|
163
165
|
</g>
|
|
164
166
|
</g>
|
|
165
|
-
</template>
|
|
167
|
+
</template>
|
|
@@ -142,6 +142,30 @@
|
|
|
142
142
|
centerRef.value.onCanvasMouseUp()
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
function onExportSvg() {
|
|
146
|
+
ElMessageBox.prompt(
|
|
147
|
+
'请输入文件名',
|
|
148
|
+
'保存为SVG文件',
|
|
149
|
+
{
|
|
150
|
+
cancelButtonText: '取消',
|
|
151
|
+
confirmButtonText: '保存',
|
|
152
|
+
inputValue: 'svg-canvas',
|
|
153
|
+
inputPattern: /^[^\\/:*?"<>|]+$/,
|
|
154
|
+
inputErrorMessage: '文件名不能包含 \\ / : * ? " < > |'
|
|
155
|
+
},
|
|
156
|
+
appContext
|
|
157
|
+
)
|
|
158
|
+
.then((r: any) => {
|
|
159
|
+
const success = centerRef.value?.exportSvg(r.value.trim())
|
|
160
|
+
if (success) {
|
|
161
|
+
ElMessage.success('SVG文件已保存')
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
.catch((e: any) => {
|
|
165
|
+
console.log(e)
|
|
166
|
+
})
|
|
167
|
+
}
|
|
168
|
+
|
|
145
169
|
onMounted(() => {
|
|
146
170
|
configStore.net.mqtt.useGlobalMqtt = Boolean(props.mqtt?.cover)
|
|
147
171
|
if (!configStore.net.mqtt.global) {
|
|
@@ -171,6 +195,7 @@
|
|
|
171
195
|
@on-return="onReturnClick"
|
|
172
196
|
@on-preview="(val: IDataModel) => emits('onPreview', val)"
|
|
173
197
|
@on-save="save"
|
|
198
|
+
@on-export-svg="onExportSvg"
|
|
174
199
|
></top-panel>
|
|
175
200
|
</el-header>
|
|
176
201
|
<el-container class="middle">
|
|
@@ -337,4 +362,4 @@
|
|
|
337
362
|
margin-top: 5px;
|
|
338
363
|
}
|
|
339
364
|
}
|
|
340
|
-
</style>
|
|
365
|
+
</style>
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
const globalStore = useGlobalStore(pinia)
|
|
15
15
|
const editPrivateStore = useEditPrivateStore(pinia)
|
|
16
16
|
const configStore = useConfigStore(pinia)
|
|
17
|
-
const emits = defineEmits(['changeVisible', 'onReturn', 'onPreview', 'onSave'])
|
|
17
|
+
const emits = defineEmits(['changeVisible', 'onReturn', 'onPreview', 'onSave', 'onExportSvg'])
|
|
18
18
|
const onDeleteBtnClick = () => {
|
|
19
19
|
globalStore.done_json.length <= 0 || globalStore.setDoneJson([])
|
|
20
20
|
globalStore.intention = EGlobalStoreIntention.None
|
|
@@ -105,6 +105,9 @@
|
|
|
105
105
|
>
|
|
106
106
|
<svg-analysis name="export" />
|
|
107
107
|
</el-icon>
|
|
108
|
+
<el-icon title="保存为SVG文件" :size="24" class="bt-Icon ml-5px" @click="emits('onExportSvg')">
|
|
109
|
+
<svg-analysis name="svg-file" />
|
|
110
|
+
</el-icon>
|
|
108
111
|
<el-divider direction="vertical" />
|
|
109
112
|
<el-icon
|
|
110
113
|
title="创建连线"
|
|
@@ -238,4 +241,4 @@
|
|
|
238
241
|
line-height: 1.9;
|
|
239
242
|
}
|
|
240
243
|
}
|
|
241
|
-
</style>
|
|
244
|
+
</style>
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
const SVG_NS = 'http://www.w3.org/2000/svg'
|
|
2
|
+
const XLINK_NS = 'http://www.w3.org/1999/xlink'
|
|
3
|
+
const XHTML_NS = 'http://www.w3.org/1999/xhtml'
|
|
4
|
+
|
|
5
|
+
const HTML_STYLE_KEYS = [
|
|
6
|
+
'display',
|
|
7
|
+
'position',
|
|
8
|
+
'top',
|
|
9
|
+
'right',
|
|
10
|
+
'bottom',
|
|
11
|
+
'left',
|
|
12
|
+
'float',
|
|
13
|
+
'clear',
|
|
14
|
+
'visibility',
|
|
15
|
+
'opacity',
|
|
16
|
+
'z-index',
|
|
17
|
+
'box-sizing',
|
|
18
|
+
'width',
|
|
19
|
+
'height',
|
|
20
|
+
'min-width',
|
|
21
|
+
'min-height',
|
|
22
|
+
'max-width',
|
|
23
|
+
'max-height',
|
|
24
|
+
'margin-top',
|
|
25
|
+
'margin-right',
|
|
26
|
+
'margin-bottom',
|
|
27
|
+
'margin-left',
|
|
28
|
+
'padding-top',
|
|
29
|
+
'padding-right',
|
|
30
|
+
'padding-bottom',
|
|
31
|
+
'padding-left',
|
|
32
|
+
'overflow',
|
|
33
|
+
'overflow-x',
|
|
34
|
+
'overflow-y',
|
|
35
|
+
'border-top-width',
|
|
36
|
+
'border-right-width',
|
|
37
|
+
'border-bottom-width',
|
|
38
|
+
'border-left-width',
|
|
39
|
+
'border-top-style',
|
|
40
|
+
'border-right-style',
|
|
41
|
+
'border-bottom-style',
|
|
42
|
+
'border-left-style',
|
|
43
|
+
'border-top-color',
|
|
44
|
+
'border-right-color',
|
|
45
|
+
'border-bottom-color',
|
|
46
|
+
'border-left-color',
|
|
47
|
+
'border-top-left-radius',
|
|
48
|
+
'border-top-right-radius',
|
|
49
|
+
'border-bottom-left-radius',
|
|
50
|
+
'border-bottom-right-radius',
|
|
51
|
+
'background-color',
|
|
52
|
+
'background-image',
|
|
53
|
+
'background-position',
|
|
54
|
+
'background-size',
|
|
55
|
+
'background-repeat',
|
|
56
|
+
'box-shadow',
|
|
57
|
+
'color',
|
|
58
|
+
'font-family',
|
|
59
|
+
'font-size',
|
|
60
|
+
'font-weight',
|
|
61
|
+
'font-style',
|
|
62
|
+
'line-height',
|
|
63
|
+
'letter-spacing',
|
|
64
|
+
'text-align',
|
|
65
|
+
'text-decoration-line',
|
|
66
|
+
'text-decoration-color',
|
|
67
|
+
'text-transform',
|
|
68
|
+
'white-space',
|
|
69
|
+
'word-break',
|
|
70
|
+
'vertical-align',
|
|
71
|
+
'list-style-type',
|
|
72
|
+
'border-collapse',
|
|
73
|
+
'border-spacing',
|
|
74
|
+
'table-layout',
|
|
75
|
+
'flex-direction',
|
|
76
|
+
'flex-wrap',
|
|
77
|
+
'flex-grow',
|
|
78
|
+
'flex-shrink',
|
|
79
|
+
'flex-basis',
|
|
80
|
+
'align-items',
|
|
81
|
+
'align-content',
|
|
82
|
+
'align-self',
|
|
83
|
+
'justify-content',
|
|
84
|
+
'order',
|
|
85
|
+
'gap',
|
|
86
|
+
'row-gap',
|
|
87
|
+
'column-gap',
|
|
88
|
+
'grid-template-columns',
|
|
89
|
+
'grid-template-rows',
|
|
90
|
+
'grid-column',
|
|
91
|
+
'grid-row'
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
const SKIP_STYLE_KEYS = new Set([
|
|
95
|
+
'animation',
|
|
96
|
+
'animation-name',
|
|
97
|
+
'animation-duration',
|
|
98
|
+
'animation-delay',
|
|
99
|
+
'animation-timing-function',
|
|
100
|
+
'animation-iteration-count',
|
|
101
|
+
'animation-direction',
|
|
102
|
+
'animation-fill-mode',
|
|
103
|
+
'animation-play-state',
|
|
104
|
+
'transition',
|
|
105
|
+
'transition-property',
|
|
106
|
+
'transition-duration',
|
|
107
|
+
'transition-delay',
|
|
108
|
+
'transition-timing-function',
|
|
109
|
+
'transform',
|
|
110
|
+
'transform-origin',
|
|
111
|
+
'transform-box',
|
|
112
|
+
'cursor',
|
|
113
|
+
'pointer-events',
|
|
114
|
+
'will-change',
|
|
115
|
+
'user-select'
|
|
116
|
+
])
|
|
117
|
+
|
|
118
|
+
interface IExportSvgOptions {
|
|
119
|
+
filename?: string
|
|
120
|
+
background?: string
|
|
121
|
+
padding?: number
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
const inlineComputedStyle = (source: Element, clone: Element) => {
|
|
125
|
+
if (source.namespaceURI !== SVG_NS) {
|
|
126
|
+
const computed = window.getComputedStyle(source as HTMLElement)
|
|
127
|
+
const style = (clone as HTMLElement).style
|
|
128
|
+
HTML_STYLE_KEYS.forEach((key) => {
|
|
129
|
+
if (SKIP_STYLE_KEYS.has(key)) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
const value = computed.getPropertyValue(key)
|
|
133
|
+
if (value) {
|
|
134
|
+
style.setProperty(key, value)
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
const sourceChildren = source.children
|
|
139
|
+
const cloneChildren = clone.children
|
|
140
|
+
const length = Math.min(sourceChildren.length, cloneChildren.length)
|
|
141
|
+
for (let i = 0; i < length; i++) {
|
|
142
|
+
inlineComputedStyle(sourceChildren[i], cloneChildren[i])
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const inlineForeignObjectStyles = (sourceSvg: Element, cloneSvg: Element) => {
|
|
147
|
+
const sourceObjects = Array.from(sourceSvg.querySelectorAll('foreignObject'))
|
|
148
|
+
const cloneObjects = Array.from(cloneSvg.querySelectorAll('foreignObject'))
|
|
149
|
+
const length = Math.min(sourceObjects.length, cloneObjects.length)
|
|
150
|
+
for (let i = 0; i < length; i++) {
|
|
151
|
+
const sourceRoot = sourceObjects[i].firstElementChild
|
|
152
|
+
const cloneRoot = cloneObjects[i].firstElementChild
|
|
153
|
+
if (sourceRoot && cloneRoot) {
|
|
154
|
+
inlineComputedStyle(sourceRoot, cloneRoot)
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const replaceCanvas = (sourceSvg: Element, cloneSvg: Element) => {
|
|
160
|
+
const sourceCanvasList = Array.from(sourceSvg.querySelectorAll('foreignObject canvas')) as HTMLCanvasElement[]
|
|
161
|
+
const cloneCanvasList = Array.from(cloneSvg.querySelectorAll('foreignObject canvas')) as HTMLCanvasElement[]
|
|
162
|
+
const length = Math.min(sourceCanvasList.length, cloneCanvasList.length)
|
|
163
|
+
for (let i = 0; i < length; i++) {
|
|
164
|
+
const sourceCanvas = sourceCanvasList[i]
|
|
165
|
+
const cloneCanvas = cloneCanvasList[i]
|
|
166
|
+
try {
|
|
167
|
+
const dataUrl = sourceCanvas.toDataURL('image/png')
|
|
168
|
+
const computed = window.getComputedStyle(sourceCanvas)
|
|
169
|
+
const img = document.createElementNS(XHTML_NS, 'img')
|
|
170
|
+
img.setAttribute('src', dataUrl)
|
|
171
|
+
img.setAttribute('style', `display:block;width:${computed.width};height:${computed.height};`)
|
|
172
|
+
cloneCanvas.replaceWith(img)
|
|
173
|
+
} catch (e) {
|
|
174
|
+
console.warn('export svg: canvas 转换失败', e)
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const collectUseHref = (root: Element, ids: Set<string>) => {
|
|
180
|
+
root.querySelectorAll('use').forEach((use) => {
|
|
181
|
+
const href = use.getAttribute('xlink:href') || use.getAttribute('href')
|
|
182
|
+
if (href && href.startsWith('#')) {
|
|
183
|
+
ids.add(href.slice(1))
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const copySymbols = (cloneSvg: Element, defs: Element) => {
|
|
189
|
+
const usedIds = new Set<string>()
|
|
190
|
+
collectUseHref(cloneSvg, usedIds)
|
|
191
|
+
|
|
192
|
+
const existingIds = new Set<string>()
|
|
193
|
+
cloneSvg.querySelectorAll('[id]').forEach((el) => {
|
|
194
|
+
const id = el.getAttribute('id')
|
|
195
|
+
if (id) {
|
|
196
|
+
existingIds.add(id)
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const pending = Array.from(usedIds)
|
|
201
|
+
while (pending.length > 0) {
|
|
202
|
+
const id = pending.shift()
|
|
203
|
+
if (!id || existingIds.has(id)) {
|
|
204
|
+
continue
|
|
205
|
+
}
|
|
206
|
+
const sourceSymbol = document.getElementById(id)
|
|
207
|
+
if (!sourceSymbol) {
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
defs.appendChild(sourceSymbol.cloneNode(true))
|
|
211
|
+
existingIds.add(id)
|
|
212
|
+
const childIds = new Set<string>()
|
|
213
|
+
collectUseHref(sourceSymbol, childIds)
|
|
214
|
+
childIds.forEach((childId) => {
|
|
215
|
+
if (!existingIds.has(childId) && pending.indexOf(childId) < 0) {
|
|
216
|
+
pending.push(childId)
|
|
217
|
+
}
|
|
218
|
+
})
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const removeEditorOnly = (cloneSvg: Element) => {
|
|
223
|
+
cloneSvg.querySelectorAll('[data-editor-only]').forEach((el) => el.remove())
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const computeContentBound = (content: Element | null) => {
|
|
227
|
+
if (!content) {
|
|
228
|
+
return null
|
|
229
|
+
}
|
|
230
|
+
const editorOnlyList = Array.from(content.querySelectorAll('[data-editor-only]')) as HTMLElement[]
|
|
231
|
+
const oldDisplayList = editorOnlyList.map((el) => el.style.display)
|
|
232
|
+
let box: SVGRect | null = null
|
|
233
|
+
try {
|
|
234
|
+
editorOnlyList.forEach((el) => (el.style.display = 'none'))
|
|
235
|
+
box = (content as SVGGraphicsElement).getBBox()
|
|
236
|
+
} catch (e) {
|
|
237
|
+
console.warn('export svg: 计算画布内容边界失败', e)
|
|
238
|
+
} finally {
|
|
239
|
+
editorOnlyList.forEach((el, index) => (el.style.display = oldDisplayList[index]))
|
|
240
|
+
}
|
|
241
|
+
return box
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const collectAnimationClassNames = (svg: Element) => {
|
|
245
|
+
const names = new Set<string>()
|
|
246
|
+
svg.querySelectorAll('[class]').forEach((el) => {
|
|
247
|
+
Array.from(el.classList).forEach((name) => {
|
|
248
|
+
if (name.indexOf('animate__') === 0) {
|
|
249
|
+
names.add(name)
|
|
250
|
+
}
|
|
251
|
+
})
|
|
252
|
+
})
|
|
253
|
+
return names
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const matchAnimationClass = (selectorText: string, classNames: Set<string>) => {
|
|
257
|
+
let matched = false
|
|
258
|
+
classNames.forEach((name) => {
|
|
259
|
+
if (selectorText.indexOf(`.${name}`) > -1) {
|
|
260
|
+
matched = true
|
|
261
|
+
}
|
|
262
|
+
})
|
|
263
|
+
return matched
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const walkStyleRules = (rules: CSSRuleList, onRule: (rule: CSSStyleRule) => void) => {
|
|
267
|
+
Array.from(rules).forEach((rule) => {
|
|
268
|
+
if (rule instanceof CSSStyleRule) {
|
|
269
|
+
onRule(rule)
|
|
270
|
+
} else if ((rule as CSSMediaRule).cssRules) {
|
|
271
|
+
walkStyleRules((rule as CSSMediaRule).cssRules, onRule)
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const collectAnimationCss = (svg: Element) => {
|
|
277
|
+
const classNames = collectAnimationClassNames(svg)
|
|
278
|
+
if (classNames.size === 0) {
|
|
279
|
+
return ''
|
|
280
|
+
}
|
|
281
|
+
const styleRules: string[] = []
|
|
282
|
+
const keyframesNames = new Set<string>()
|
|
283
|
+
const keyframesRules: string[] = []
|
|
284
|
+
|
|
285
|
+
Array.from(document.styleSheets).forEach((sheet) => {
|
|
286
|
+
let rules: CSSRuleList | null = null
|
|
287
|
+
try {
|
|
288
|
+
rules = sheet.cssRules
|
|
289
|
+
} catch (e) {
|
|
290
|
+
rules = null
|
|
291
|
+
}
|
|
292
|
+
if (!rules) {
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
walkStyleRules(rules, (rule) => {
|
|
296
|
+
if (!matchAnimationClass(rule.selectorText, classNames)) {
|
|
297
|
+
return
|
|
298
|
+
}
|
|
299
|
+
styleRules.push(rule.cssText)
|
|
300
|
+
const animationName = rule.style.getPropertyValue('animation-name')
|
|
301
|
+
if (animationName) {
|
|
302
|
+
animationName.split(',').forEach((name) => keyframesNames.add(name.trim()))
|
|
303
|
+
}
|
|
304
|
+
})
|
|
305
|
+
Array.from(rules).forEach((rule) => {
|
|
306
|
+
if (rule instanceof CSSKeyframesRule && keyframesNames.has(rule.name)) {
|
|
307
|
+
keyframesRules.push(rule.cssText)
|
|
308
|
+
}
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
if (styleRules.length === 0) {
|
|
313
|
+
return ''
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
keyframesNames.forEach((name) => {
|
|
317
|
+
if (keyframesRules.some((css) => css.indexOf(`@keyframes ${name}`) > -1)) {
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
Array.from(document.styleSheets).forEach((sheet) => {
|
|
321
|
+
let rules: CSSRuleList | null = null
|
|
322
|
+
try {
|
|
323
|
+
rules = sheet.cssRules
|
|
324
|
+
} catch (e) {
|
|
325
|
+
rules = null
|
|
326
|
+
}
|
|
327
|
+
if (!rules) {
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
Array.from(rules).forEach((rule) => {
|
|
331
|
+
if (rule instanceof CSSKeyframesRule && rule.name === name) {
|
|
332
|
+
keyframesRules.push(rule.cssText)
|
|
333
|
+
}
|
|
334
|
+
})
|
|
335
|
+
})
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
return [...styleRules, ...keyframesRules].join('\n')
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export const getCanvasSvgString = (svg: SVGSVGElement, options: IExportSvgOptions = {}) => {
|
|
342
|
+
const padding = options.padding ?? 20
|
|
343
|
+
const content = svg.querySelector('[data-canvas-content]')
|
|
344
|
+
let box: SVGRect | null = computeContentBound(content)
|
|
345
|
+
|
|
346
|
+
if (!box || !box.width || !box.height) {
|
|
347
|
+
const rect = svg.getBoundingClientRect()
|
|
348
|
+
box = {
|
|
349
|
+
x: 0,
|
|
350
|
+
y: 0,
|
|
351
|
+
width: rect.width || 800,
|
|
352
|
+
height: rect.height || 600
|
|
353
|
+
} as SVGRect
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const viewBox = {
|
|
357
|
+
x: box.x - padding,
|
|
358
|
+
y: box.y - padding,
|
|
359
|
+
width: box.width + padding * 2,
|
|
360
|
+
height: box.height + padding * 2
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const clone = svg.cloneNode(true) as SVGSVGElement
|
|
364
|
+
removeEditorOnly(clone)
|
|
365
|
+
|
|
366
|
+
const cloneContent = clone.querySelector('[data-canvas-content]')
|
|
367
|
+
if (cloneContent) {
|
|
368
|
+
cloneContent.removeAttribute('transform')
|
|
369
|
+
cloneContent.removeAttribute('data-canvas-content')
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
replaceCanvas(svg, clone)
|
|
373
|
+
inlineForeignObjectStyles(svg, clone)
|
|
374
|
+
|
|
375
|
+
clone.setAttribute('xmlns', SVG_NS)
|
|
376
|
+
clone.setAttribute('xmlns:xlink', XLINK_NS)
|
|
377
|
+
clone.setAttribute('width', `${Math.ceil(viewBox.width)}`)
|
|
378
|
+
clone.setAttribute('height', `${Math.ceil(viewBox.height)}`)
|
|
379
|
+
clone.setAttribute('viewBox', `${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`)
|
|
380
|
+
|
|
381
|
+
const defs = (clone.querySelector('defs') as Element) || document.createElementNS(SVG_NS, 'defs')
|
|
382
|
+
if (!defs.parentNode) {
|
|
383
|
+
clone.insertBefore(defs, clone.firstChild)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
copySymbols(clone, defs)
|
|
387
|
+
|
|
388
|
+
const style = document.createElementNS(SVG_NS, 'style')
|
|
389
|
+
const animationCss = collectAnimationCss(svg)
|
|
390
|
+
style.textContent = `.common-ani{transform-box:fill-box;}${animationCss ? `\n${animationCss}` : ''}`
|
|
391
|
+
defs.appendChild(style)
|
|
392
|
+
|
|
393
|
+
if (options.background) {
|
|
394
|
+
const background = document.createElementNS(SVG_NS, 'rect')
|
|
395
|
+
background.setAttribute('x', `${viewBox.x}`)
|
|
396
|
+
background.setAttribute('y', `${viewBox.y}`)
|
|
397
|
+
background.setAttribute('width', `${viewBox.width}`)
|
|
398
|
+
background.setAttribute('height', `${viewBox.height}`)
|
|
399
|
+
background.setAttribute('fill', options.background)
|
|
400
|
+
clone.insertBefore(background, clone.firstChild)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return `<?xml version="1.0" encoding="UTF-8"?>\n${new XMLSerializer().serializeToString(clone)}`
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
export const downloadSvgFile = (content: string, filename?: string) => {
|
|
407
|
+
const name = (filename || `svg-canvas-${Date.now()}`).replace(/\.svg$/i, '')
|
|
408
|
+
const blob = new Blob([content], { type: 'image/svg+xml;charset=utf-8' })
|
|
409
|
+
const url = URL.createObjectURL(blob)
|
|
410
|
+
const link = document.createElement('a')
|
|
411
|
+
link.href = url
|
|
412
|
+
link.download = `${name}.svg`
|
|
413
|
+
link.style.display = 'none'
|
|
414
|
+
document.body.appendChild(link)
|
|
415
|
+
link.click()
|
|
416
|
+
document.body.removeChild(link)
|
|
417
|
+
setTimeout(() => URL.revokeObjectURL(url), 0)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
export const exportCanvasSvg = (svg: SVGSVGElement, options: IExportSvgOptions = {}) => {
|
|
421
|
+
const content = getCanvasSvgString(svg, options)
|
|
422
|
+
downloadSvgFile(content, options.filename)
|
|
423
|
+
}
|