jvs-ppt-ui 0.0.15 → 0.0.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.
Files changed (27) hide show
  1. package/dist/lib/jvs-ppt-ui.es.js +20301 -19823
  2. package/dist/lib/jvs-ppt-ui.umd.js +99 -99
  3. package/dist/lib/style.css +1 -1
  4. package/package.json +1 -1
  5. package/src/components/CanvasArea.vue +14 -115
  6. package/src/components/ChartTypeSelector.vue +34 -30
  7. package/src/components/Header.vue +40 -5
  8. package/src/components/PresentationGroupChild.vue +44 -13
  9. package/src/components/PresentationMode.vue +84 -33
  10. package/src/components/SidebarLeft.vue +96 -18
  11. package/src/components/SidebarRight.vue +6 -3
  12. package/src/components/Toolbar.vue +1 -1
  13. package/src/components/canvas/elements/CanvasShapeElement.vue +91 -34
  14. package/src/components/sidebar/common/SidebarBorderAccordion.vue +2 -2
  15. package/src/components/sidebar/common/SidebarReflectionAccordion.vue +2 -2
  16. package/src/components/sidebar/common/SidebarShadowAccordion.vue +2 -2
  17. package/src/components/sidebar/panels/SidebarChartFormatPanel.vue +3 -3
  18. package/src/components/sidebar/panels/SidebarDesignPanel.vue +170 -9
  19. package/src/components/sidebar/panels/SidebarShapeFormatPanel.vue +176 -16
  20. package/src/components/sidebar/panels/SidebarTableFormatPanel.vue +2 -2
  21. package/src/components/sidebar/panels/SidebarTextFormatPanel.vue +2 -2
  22. package/src/components/sidebar/panels/SidebarTransitionPanel.vue +30 -19
  23. package/src/composables/canvas/useCanvasTransform.ts +24 -0
  24. package/src/composables/sidebar/useSidebarShapeFormat.ts +248 -17
  25. package/src/composables/useSlides.ts +12 -0
  26. package/src/style.css +4 -2
  27. package/src/utils/canvas/elementStyles.ts +22 -0
@@ -96,15 +96,128 @@ export function generateGradientEndColor(hex: string): string {
96
96
  }
97
97
  }
98
98
 
99
+ function encodeSvgColor(color: string, fallback = '#3B82F6'): string {
100
+ const c = (color || fallback).trim()
101
+ if (!c || c === 'none') return 'transparent'
102
+ return c.replace(/#/g, '%23')
103
+ }
104
+
105
+ export function buildPatternSvgDataUrl(type: string, fgColor = '#3B82F6', bgColor = '#FFFFFF'): string {
106
+ const fg = encodeSvgColor(fgColor, '#3B82F6')
107
+ const bg = encodeSvgColor(bgColor, '#FFFFFF')
108
+ const rawFg = (fgColor || '#3B82F6').trim().replace(/#/g, '%23')
109
+ const rawBg = (bgColor || '#FFFFFF').trim().replace(/#/g, '%23')
110
+
111
+ let w = 10
112
+ let h = 10
113
+ let body = ''
114
+
115
+ switch (type) {
116
+ case 'dots-sparse':
117
+ w = 10; h = 10
118
+ body = `<circle cx='2.5' cy='2.5' r='1.1' fill='${fg}'/><circle cx='7.5' cy='7.5' r='1.1' fill='${fg}'/>`
119
+ break
120
+ case 'dots-dense':
121
+ w = 10; h = 10
122
+ body = `<circle cx='5' cy='5' r='2' fill='${fg}'/>`
123
+ break
124
+ case 'diag-up':
125
+ w = 10; h = 10
126
+ body = `<path d='M0 10L10 0M-2 2L2 -2M8 12L12 8' stroke='${fg}' stroke-width='1.5'/>`
127
+ break
128
+ case 'diag-down':
129
+ w = 10; h = 10
130
+ body = `<path d='M0 0L10 10M-2 8L2 12M8 -2L12 2' stroke='${fg}' stroke-width='1.5'/>`
131
+ break
132
+ case 'cross-diag':
133
+ w = 10; h = 10
134
+ body = `<path d='M0 10L10 0M0 0L10 10' stroke='${fg}' stroke-width='1.2'/>`
135
+ break
136
+ case 'grid':
137
+ w = 12; h = 12
138
+ body = `<path d='M12 0H0V12' fill='none' stroke='${fg}' stroke-width='1.2'/>`
139
+ break
140
+ case 'horizontal':
141
+ w = 8; h = 8
142
+ body = `<line x1='0' y1='4' x2='8' y2='4' stroke='${fg}' stroke-width='1.5'/>`
143
+ break
144
+ case 'vertical':
145
+ w = 8; h = 8
146
+ body = `<line x1='4' y1='0' x2='4' y2='8' stroke='${fg}' stroke-width='1.5'/>`
147
+ break
148
+ case 'brick':
149
+ w = 16; h = 16
150
+ body = `<path d='M0 8h16M8 0v8M0 16v-8' fill='none' stroke='${fg}' stroke-width='1.2'/>`
151
+ break
152
+ case 'wave':
153
+ w = 16; h = 8
154
+ body = `<path d='M0 4 Q4 0, 8 4 T16 4' fill='none' stroke='${fg}' stroke-width='1.3'/>`
155
+ break
156
+ case 'checker':
157
+ w = 12; h = 12
158
+ body = `<rect width='6' height='6' fill='${fg}'/><rect x='6' y='6' width='6' height='6' fill='${fg}'/>`
159
+ break
160
+ case 'zigzag':
161
+ w = 12; h = 8
162
+ body = `<path d='M0 6L6 2L12 6' fill='none' stroke='${fg}' stroke-width='1.3' stroke-linecap='round' stroke-linejoin='round'/>`
163
+ break
164
+ default:
165
+ w = 10; h = 10
166
+ body = `<path d='M0 10L10 0M-2 2L2 -2M8 12L12 8' stroke='${fg}' stroke-width='1.5'/>`
167
+ break
168
+ }
169
+
170
+ const svg = `<svg xmlns='http://www.w3.org/2000/svg' width='${w}' height='${h}' data-pattern-type='${type}' data-fg='${rawFg}' data-bg='${rawBg}'><rect width='${w}' height='${h}' fill='${bg}'/>${body}</svg>`
171
+ const encoded = svg
172
+ .replace(/'/g, '%22')
173
+ .replace(/</g, '%3C')
174
+ .replace(/>/g, '%3E')
175
+ .replace(/\s+/g, '%20')
176
+ return `data:image/svg+xml;utf8,${encoded}`
177
+ }
178
+
179
+ export function parsePatternSvgMeta(url?: string): { type?: string; fg?: string; bg?: string } {
180
+ if (!url || typeof url !== 'string' || !url.startsWith('data:image/svg+xml')) return {}
181
+ let decoded = url
182
+ try {
183
+ decoded = decodeURIComponent(url)
184
+ } catch {
185
+ decoded = url.replace(/%22/g, '"').replace(/%27/g, "'").replace(/%23/g, '#')
186
+ }
187
+ const typeMatch = decoded.match(/data-pattern-type=['"]([^'"]+)['"]/)
188
+ const fgMatch = decoded.match(/data-fg=['"]([^'"]+)['"]/)
189
+ const bgMatch = decoded.match(/data-bg=['"]([^'"]+)['"]/)
190
+ let type = typeMatch ? typeMatch[1] : undefined
191
+ if (!type) {
192
+ if (decoded.includes('M0 10L10 0M0 0L10 10')) type = 'cross-diag'
193
+ else if (decoded.includes('M0 10L10 0')) type = 'diag-up'
194
+ else if (decoded.includes('M12 0H0V12')) type = 'grid'
195
+ else if (decoded.includes('cx="5" cy="5" r="2"') || decoded.includes("cx='5' cy='5' r='2'")) type = 'dots-dense'
196
+ else if (decoded.includes('M0 8h16')) type = 'brick'
197
+ else if (decoded.includes('y1="4" x2="8"') || decoded.includes("y1='4' x2='8'")) type = 'horizontal'
198
+ else if (decoded.includes('Q4 0, 8 4')) type = 'wave'
199
+ else if (decoded.includes('width="6" height="6"') || decoded.includes("width='6' height='6'")) type = 'checker'
200
+ }
201
+ return {
202
+ type,
203
+ fg: fgMatch ? fgMatch[1].replace(/%23/g, '#') : undefined,
204
+ bg: bgMatch ? bgMatch[1].replace(/%23/g, '#') : undefined
205
+ }
206
+ }
207
+
99
208
  export const PATTERN_PRESETS = [
100
- { name: '斜线', url: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><rect width='10' height='10' fill='%23ffffff'/><path d='M0 10L10 0M-2 2L2 -2M8 12L12 8' stroke='%233b82f6' stroke-width='1.5'/></svg>" },
101
- { name: '网格', url: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12'><rect width='12' height='12' fill='%23ffffff'/><path d='M12 0H0V12' fill='none' stroke='%233b82f6' stroke-width='1.2'/></svg>" },
102
- { name: '圆点', url: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><rect width='10' height='10' fill='%23ffffff'/><circle cx='5' cy='5' r='2' fill='%233b82f6'/></svg>" },
103
- { name: '砖块', url: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='16'><rect width='16' height='16' fill='%23ffffff'/><path d='M0 8h16M8 0v8M0 16v-8' fill='none' stroke='%233b82f6' stroke-width='1.2'/></svg>" },
104
- { name: '横线', url: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='8' height='8'><rect width='8' height='8' fill='%23ffffff'/><line x1='0' y1='4' x2='8' y2='4' stroke='%233b82f6' stroke-width='1.5'/></svg>" },
105
- { name: '波浪', url: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='16' height='8'><rect width='16' height='8' fill='%23ffffff'/><path d='M0 4 Q4 0, 8 4 T16 4' fill='none' stroke='%233b82f6' stroke-width='1.2'/></svg>" },
106
- { name: '棋盘格', url: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='12' height='12'><rect width='12' height='12' fill='%23ffffff'/><rect width='6' height='6' fill='%233b82f6'/><rect x='6' y='6' width='6' height='6' fill='%233b82f6'/></svg>" },
107
- { name: '交叉线', url: "data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='10'><rect width='10' height='10' fill='%23ffffff'/><path d='M0 10L10 0M0 0L10 10' stroke='%233b82f6' stroke-width='1.2'/></svg>" }
209
+ { key: 'dots-sparse', name: '稀疏圆点', url: buildPatternSvgDataUrl('dots-sparse', '#3B82F6', '#FFFFFF') },
210
+ { key: 'dots-dense', name: '密集圆点', url: buildPatternSvgDataUrl('dots-dense', '#3B82F6', '#FFFFFF') },
211
+ { key: 'diag-up', name: '上对角线', url: buildPatternSvgDataUrl('diag-up', '#3B82F6', '#FFFFFF') },
212
+ { key: 'diag-down', name: '下对角线', url: buildPatternSvgDataUrl('diag-down', '#3B82F6', '#FFFFFF') },
213
+ { key: 'cross-diag', name: '交叉斜线', url: buildPatternSvgDataUrl('cross-diag', '#3B82F6', '#FFFFFF') },
214
+ { key: 'grid', name: '细网格', url: buildPatternSvgDataUrl('grid', '#3B82F6', '#FFFFFF') },
215
+ { key: 'horizontal', name: '横线', url: buildPatternSvgDataUrl('horizontal', '#3B82F6', '#FFFFFF') },
216
+ { key: 'vertical', name: '竖线', url: buildPatternSvgDataUrl('vertical', '#3B82F6', '#FFFFFF') },
217
+ { key: 'brick', name: '砖块', url: buildPatternSvgDataUrl('brick', '#3B82F6', '#FFFFFF') },
218
+ { key: 'wave', name: '波浪', url: buildPatternSvgDataUrl('wave', '#3B82F6', '#FFFFFF') },
219
+ { key: 'checker', name: '棋盘格', url: buildPatternSvgDataUrl('checker', '#3B82F6', '#FFFFFF') },
220
+ { key: 'zigzag', name: '之字形', url: buildPatternSvgDataUrl('zigzag', '#3B82F6', '#FFFFFF') }
108
221
  ]
109
222
 
110
223
  export function useSidebarShapeFormat(
@@ -359,7 +472,7 @@ export function useSidebarShapeFormat(
359
472
  const patternVal = el.pattern || el.style?.pattern || (rawFill && typeof rawFill === 'object' && (rawFill.type === 'image' || rawFill.type === 'pattern') ? (rawFill.value?.src || rawFill.value?.url || rawFill.value) : '')
360
473
 
361
474
  if (hasPattern || patternVal) {
362
- if (patternVal && PATTERN_PRESETS.some(item => item.url === patternVal)) {
475
+ if (patternVal && (String(patternVal).startsWith('data:image/svg+xml') || el.patternType || el.style?.patternType || PATTERN_PRESETS.some(item => item.url === patternVal))) {
363
476
  return 'pattern'
364
477
  }
365
478
  return 'image'
@@ -444,7 +557,7 @@ export function useSidebarShapeFormat(
444
557
  delete el.gradient
445
558
  delete el.style.gradient
446
559
  const existing = el.pattern || el.style?.pattern || ''
447
- const isPreset = PATTERN_PRESETS.some(item => item.url === existing)
560
+ const isPreset = String(existing).startsWith('data:image/svg+xml') || PATTERN_PRESETS.some(item => item.url === existing)
448
561
  const patVal = (!existing || isPreset) ? '' : existing
449
562
  el.pattern = patVal
450
563
  el.style.pattern = patVal
@@ -458,9 +571,18 @@ export function useSidebarShapeFormat(
458
571
  }
459
572
  delete el.gradient
460
573
  delete el.style.gradient
461
- const defaultPat = PATTERN_PRESETS[0].url
462
- el.pattern = defaultPat
463
- el.style.pattern = defaultPat
574
+ const pType = el.patternType || el.style?.patternType || 'diag-up'
575
+ const pFg = el.patternFg || el.style?.patternFg || (el._lastSolidFill && el._lastSolidFill !== 'none' && el._lastSolidFill !== 'transparent' ? el._lastSolidFill : '#3B82F6')
576
+ const pBg = el.patternBg || el.style?.patternBg || '#FFFFFF'
577
+ el.patternType = pType
578
+ el.patternFg = pFg
579
+ el.patternBg = pBg
580
+ el.style.patternType = pType
581
+ el.style.patternFg = pFg
582
+ el.style.patternBg = pBg
583
+ const patUrl = buildPatternSvgDataUrl(pType, pFg, pBg)
584
+ el.pattern = patUrl
585
+ el.style.pattern = patUrl
464
586
  el.style.fill = 'none'
465
587
  }
466
588
  })
@@ -848,10 +970,116 @@ export function useSidebarShapeFormat(
848
970
  },
849
971
  set(val: string) {
850
972
  if (!selectedElement.value) return
851
- if (!selectedElement.value.style) selectedElement.value.style = {}
852
- selectedElement.value.pattern = val
853
- selectedElement.value.style.pattern = val
854
- selectedElement.value.style.fill = 'none'
973
+ const elements = getFormattingElements()
974
+ const meta = parsePatternSvgMeta(val)
975
+ elements.forEach((el: any) => {
976
+ if (!el.style) el.style = {}
977
+ if (meta.type) {
978
+ el.patternType = meta.type
979
+ el.style.patternType = meta.type
980
+ }
981
+ if (meta.fg) {
982
+ el.patternFg = meta.fg
983
+ el.style.patternFg = meta.fg
984
+ }
985
+ if (meta.bg) {
986
+ el.patternBg = meta.bg
987
+ el.style.patternBg = meta.bg
988
+ }
989
+ el.pattern = val
990
+ el.style.pattern = val
991
+ el.style.fill = 'none'
992
+ })
993
+ addHistorySnapshot()
994
+ }
995
+ })
996
+
997
+ const shapePatternType = computed({
998
+ get() {
999
+ if (!selectedElement.value) return 'diag-up'
1000
+ const el = selectedElement.value
1001
+ if (el.patternType || el.style?.patternType) return el.patternType || el.style.patternType
1002
+ const meta = parsePatternSvgMeta(el.pattern || el.style?.pattern)
1003
+ return meta.type || 'diag-up'
1004
+ },
1005
+ set(val: string) {
1006
+ if (!selectedElement.value) return
1007
+ const elements = getFormattingElements()
1008
+ const fg = shapePatternFg.value || '#3B82F6'
1009
+ const bg = shapePatternBg.value || '#FFFFFF'
1010
+ const url = buildPatternSvgDataUrl(val, fg, bg)
1011
+ elements.forEach((el: any) => {
1012
+ if (!el.style) el.style = {}
1013
+ el.patternType = val
1014
+ el.style.patternType = val
1015
+ el.patternFg = fg
1016
+ el.style.patternFg = fg
1017
+ el.patternBg = bg
1018
+ el.style.patternBg = bg
1019
+ el.pattern = url
1020
+ el.style.pattern = url
1021
+ el.style.fill = 'none'
1022
+ })
1023
+ addHistorySnapshot()
1024
+ }
1025
+ })
1026
+
1027
+ const shapePatternFg = computed({
1028
+ get() {
1029
+ if (!selectedElement.value) return '#3B82F6'
1030
+ const el = selectedElement.value
1031
+ if (el.patternFg || el.style?.patternFg) return el.patternFg || el.style.patternFg
1032
+ const meta = parsePatternSvgMeta(el.pattern || el.style?.pattern)
1033
+ return meta.fg || '#3B82F6'
1034
+ },
1035
+ set(val: string) {
1036
+ if (!selectedElement.value) return
1037
+ const elements = getFormattingElements()
1038
+ const pType = shapePatternType.value || 'diag-up'
1039
+ const bg = shapePatternBg.value || '#FFFFFF'
1040
+ const url = buildPatternSvgDataUrl(pType, val, bg)
1041
+ elements.forEach((el: any) => {
1042
+ if (!el.style) el.style = {}
1043
+ el.patternType = pType
1044
+ el.style.patternType = pType
1045
+ el.patternFg = val
1046
+ el.style.patternFg = val
1047
+ el.patternBg = bg
1048
+ el.style.patternBg = bg
1049
+ el.pattern = url
1050
+ el.style.pattern = url
1051
+ el.style.fill = 'none'
1052
+ })
1053
+ addHistorySnapshot()
1054
+ }
1055
+ })
1056
+
1057
+ const shapePatternBg = computed({
1058
+ get() {
1059
+ if (!selectedElement.value) return '#FFFFFF'
1060
+ const el = selectedElement.value
1061
+ if (el.patternBg || el.style?.patternBg) return el.patternBg || el.style.patternBg
1062
+ const meta = parsePatternSvgMeta(el.pattern || el.style?.pattern)
1063
+ return meta.bg || '#FFFFFF'
1064
+ },
1065
+ set(val: string) {
1066
+ if (!selectedElement.value) return
1067
+ const elements = getFormattingElements()
1068
+ const pType = shapePatternType.value || 'diag-up'
1069
+ const fg = shapePatternFg.value || '#3B82F6'
1070
+ const url = buildPatternSvgDataUrl(pType, fg, val)
1071
+ elements.forEach((el: any) => {
1072
+ if (!el.style) el.style = {}
1073
+ el.patternType = pType
1074
+ el.style.patternType = pType
1075
+ el.patternFg = fg
1076
+ el.style.patternFg = fg
1077
+ el.patternBg = val
1078
+ el.style.patternBg = val
1079
+ el.pattern = url
1080
+ el.style.pattern = url
1081
+ el.style.fill = 'none'
1082
+ })
855
1083
  addHistorySnapshot()
856
1084
  }
857
1085
  })
@@ -1529,6 +1757,9 @@ export function useSidebarShapeFormat(
1529
1757
  updateGradientConfig,
1530
1758
  updateElementGradient,
1531
1759
  shapePatternFill,
1760
+ shapePatternType,
1761
+ shapePatternFg,
1762
+ shapePatternBg,
1532
1763
  handleShapePatternUpload,
1533
1764
  clearShapePatternFill,
1534
1765
  styleBorderStyle,
@@ -1032,12 +1032,18 @@ function applyBackgroundToAll() {
1032
1032
  const type = activeSlide.value.bgType || 'color'
1033
1033
  const imgMode = (activeSlide.value as any).bgImageMode
1034
1034
  const isPattern = (activeSlide.value as any).isPattern
1035
+ const bgPatternType = (activeSlide.value as any).bgPatternType
1036
+ const bgPatternFg = (activeSlide.value as any).bgPatternFg
1037
+ const bgPatternBg = (activeSlide.value as any).bgPatternBg
1035
1038
  const lastImgUrl = (activeSlide.value as any)._lastBgImageUrl
1036
1039
  slides.value.forEach(slide => {
1037
1040
  slide.background = bg
1038
1041
  slide.bgType = type
1039
1042
  if (imgMode !== undefined) (slide as any).bgImageMode = imgMode
1040
1043
  if (isPattern !== undefined) (slide as any).isPattern = isPattern
1044
+ if (bgPatternType !== undefined) (slide as any).bgPatternType = bgPatternType
1045
+ if (bgPatternFg !== undefined) (slide as any).bgPatternFg = bgPatternFg
1046
+ if (bgPatternBg !== undefined) (slide as any).bgPatternBg = bgPatternBg
1041
1047
  if (lastImgUrl !== undefined) (slide as any)._lastBgImageUrl = lastImgUrl
1042
1048
  })
1043
1049
  if (isMasterEditingView.value || slideMasters.value.length > 0) {
@@ -1046,12 +1052,18 @@ function applyBackgroundToAll() {
1046
1052
  m.bgType = type
1047
1053
  if (imgMode !== undefined) (m as any).bgImageMode = imgMode
1048
1054
  if (isPattern !== undefined) (m as any).isPattern = isPattern
1055
+ if (bgPatternType !== undefined) (m as any).bgPatternType = bgPatternType
1056
+ if (bgPatternFg !== undefined) (m as any).bgPatternFg = bgPatternFg
1057
+ if (bgPatternBg !== undefined) (m as any).bgPatternBg = bgPatternBg
1049
1058
  if (lastImgUrl !== undefined) (m as any)._lastBgImageUrl = lastImgUrl
1050
1059
  m.layouts?.forEach(l => {
1051
1060
  l.background = bg
1052
1061
  l.bgType = type
1053
1062
  if (imgMode !== undefined) (l as any).bgImageMode = imgMode
1054
1063
  if (isPattern !== undefined) (l as any).isPattern = isPattern
1064
+ if (bgPatternType !== undefined) (l as any).bgPatternType = bgPatternType
1065
+ if (bgPatternFg !== undefined) (l as any).bgPatternFg = bgPatternFg
1066
+ if (bgPatternBg !== undefined) (l as any).bgPatternBg = bgPatternBg
1055
1067
  if (lastImgUrl !== undefined) (l as any)._lastBgImageUrl = lastImgUrl
1056
1068
  })
1057
1069
  })
package/src/style.css CHANGED
@@ -3841,8 +3841,10 @@ td.selected-cell.focused-cell,
3841
3841
  border: 1px solid #edf0f5 !important;
3842
3842
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08) !important;
3843
3843
  background: #ffffff !important;
3844
- width: 516px !important;
3845
- max-width: 516px !important;
3844
+ width: 568px !important;
3845
+ max-width: 568px !important;
3846
+ height: 538px !important;
3847
+ box-sizing: border-box !important;
3846
3848
  }
3847
3849
 
3848
3850
  .chart-popover-popper .el-popper__arrow {
@@ -218,6 +218,28 @@ export function getElementPattern(el: any): string | undefined {
218
218
  return `data:image/png;base64,${str}`
219
219
  }
220
220
 
221
+ export function isElementPatternTile(el: any): boolean {
222
+ const p = getElementPattern(el)
223
+ if (!p) return false
224
+ return p.startsWith('data:image/svg+xml') || Boolean(el?.patternType || el?.style?.patternType)
225
+ }
226
+
227
+ export function getElementPatternTileSize(el: any): { w: number; h: number } {
228
+ const p = getElementPattern(el)
229
+ if (!p || !p.startsWith('data:image/svg+xml')) return { w: 12, h: 12 }
230
+ let decoded = p
231
+ try {
232
+ decoded = decodeURIComponent(p)
233
+ } catch {
234
+ decoded = p.replace(/%22/g, '"').replace(/%27/g, "'")
235
+ }
236
+ const wMatch = decoded.match(/width=['"](\d+(?:\.\d+)?)['"]/)
237
+ const hMatch = decoded.match(/height=['"](\d+(?:\.\d+)?)['"]/)
238
+ const w = wMatch ? parseFloat(wMatch[1]) : 12
239
+ const h = hMatch ? parseFloat(hMatch[1]) : 12
240
+ return { w: w > 0 ? w : 12, h: h > 0 ? h : 12 }
241
+ }
242
+
221
243
  export interface GradientColorStop {
222
244
  pos: number | string
223
245
  color: string