@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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhangqingcq/vgce",
3
- "version": "0.1.35",
3
+ "version": "0.2.0",
4
4
  "description": "Vector graphics configure editor. svg组态编辑器。基于vue3.3+ts+element-plus+vite",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -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
- defineExpose({ onCanvasMouseUp })
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 id="pattern_grid" patternUnits="userSpaceOnUse" x="0" y="0" width="20" height="20">
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
+ }