jvs-ppt-ui 0.0.16 → 0.0.17

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.
@@ -1,882 +1,894 @@
1
- <script setup lang="ts">
2
- import { computed, ref, watch, onBeforeUnmount, nextTick } from 'vue'
3
- import {
4
- getElementPattern,
5
- isElementPatternTile,
6
- getElementPatternTileSize,
7
- getElementGradientConfig,
8
- getElementGradientCss,
9
- getSvgGradientCoords,
10
- getElementFill,
11
- hasElementFill,
12
- hasElementBorder,
13
- getElementBorderColor,
14
- getElementBorderWidth,
15
- getElementBorderGradientConfig,
16
- hasElementBorderGradient,
17
- getElementBorderGradientCss,
18
- isSvgPathString,
19
- getHexColorWithoutAlpha,
20
- getAlphaFromColor
21
- } from '../../../utils/canvas/elementStyles'
22
- import { isEllipseShape } from '../../../utils/line'
23
- import { getFontVariantStyle } from '../../../configs/fonts'
24
- import { useSlides } from '../../../composables/useSlides'
25
- import { createTextSelectionOverlay, type TextSelectionOverlayHandle } from '../textSelectionOverlay'
26
-
27
- const { shapeFormatPainter, isReadonly } = useSlides()
28
- const isTextPaintActive = computed(() => !!(shapeFormatPainter.value && shapeFormatPainter.value.mode === 'text-range'))
29
- const svgIdKey = computed(() => props.isReflection ? `${props.element.id}-refl` : `${props.element.id}`)
30
- const isTextSelectable = computed(() => !props.isReflection && ((props.isEditingText && props.selectedElementId === props.element.id) || isTextPaintActive.value))
31
-
32
- function getEffectiveBorderWidth(el: any): number {
33
- if (props.isImagePlaceholder && props.isImagePlaceholder(el) && el.style?.enableBorder !== true) return 0
34
- if (el.style?.enableBorder === false) return 0
35
- const w = getElementBorderWidth(el)
36
- if (w > 0) return w
37
- if (el.style?.borderColor || el.borderColor) return 1
38
- return 0
39
- }
40
-
41
- function getShapePatternTransform(el: any, isTriangle = false): string | undefined {
42
- if (!el || !isElementPatternTile(el)) return undefined
43
- const { width, height } = getElementWidthHeight(el)
44
- if (isTriangle) {
45
- const sx = width > 0 ? 100 / width : 1
46
- const sy = height > 0 ? 100 / height : 1
47
- return `scale(${sx}, ${sy})`
48
- }
49
- const vbW = el.viewBox ? el.viewBox[0] : (width || 200)
50
- const vbH = el.viewBox ? el.viewBox[1] : (height || 200)
51
- const hasBorder = hasElementBorder(el)
52
- const bw = hasBorder ? getEffectiveBorderWidth(el) : 0
53
- const effW = bw > 0 ? Math.max(1, width - bw) : Math.max(1, width)
54
- const effH = bw > 0 ? Math.max(1, height - bw) : Math.max(1, height)
55
- return `scale(${vbW / effW}, ${vbH / effH})`
56
- }
57
-
58
- function getShapeSvgTransform(el: any): string | undefined {
59
- if (!el) return undefined
60
- const { width, height } = getElementWidthHeight(el)
61
- const vbW = el.viewBox ? el.viewBox[0] : (width || 200)
62
- const vbH = el.viewBox ? el.viewBox[1] : (height || 200)
63
-
64
- if (el.shapType === 'arc') {
65
- return `scale(${width / vbW}, ${height / vbH})`
66
- }
67
- if (isOpenLineShape(el)) {
68
- return undefined
69
- }
70
-
71
- const hasBorder = hasElementBorder(el)
72
- const bw = hasBorder ? getEffectiveBorderWidth(el) : 0
73
-
74
- if (bw > 0) {
75
- const scaleX = Math.max(0, width - bw) / vbW
76
- const scaleY = Math.max(0, height - bw) / vbH
77
- return `translate(${bw / 2}, ${bw / 2}) scale(${scaleX}, ${scaleY})`
78
- }
79
-
80
- const scaleX = width / vbW
81
- const scaleY = height / vbH
82
- return `scale(${scaleX}, ${scaleY})`
83
- }
84
-
85
- function getEffectiveBorderColor(el: any, fallback = '#10b981'): string {
86
- if (props.isImagePlaceholder && props.isImagePlaceholder(el) && el.style?.enableBorder !== true) return 'transparent'
87
- if (el.style?.enableBorder === false) return 'transparent'
88
- if (hasElementBorderGradient(el)) return `url(#shape-border-gradient-${svgIdKey.value})`
89
- return el.style?.borderColor || getElementBorderColor(el) || fallback
90
- }
91
-
92
- function isOpenLineShape(el: any): boolean {
93
- if (!el) return false
94
- const shapType = el.shapType || el.type
95
- return shapType === 'arc' || shapType === 'straightConnector1' || shapType === 'line' || (typeof el.width === 'number' && el.width <= 0) || (typeof el.height === 'number' && el.height <= 0)
96
- }
97
-
98
- function getShapeVb(el: any): { w: number; h: number } {
99
- if (!el) return { w: 200, h: 200 }
100
- const { width, height } = getElementWidthHeight(el)
101
- const w = el.viewBox ? el.viewBox[0] : (width || 200)
102
- const h = el.viewBox ? el.viewBox[1] : (height || 200)
103
- return { w, h }
104
- }
105
-
106
- function isRectShape(el: any): boolean {
107
- if (!el) return false
108
- if (el.shapType === 'rect' || el.shapType === 'roundRect') return true
109
- if (el.content === 'rect') return true
110
- if (el.pathFormula === 'roundRect') return true
111
- if (el.title === '矩形' || el.title === '圆角矩形') return true
112
- const raw = (el.path || el.content || '').trim().replace(/\s+/g, '').toUpperCase()
113
- if (raw === 'M00L2000L200200L0200Z' || raw === 'M00H200V200H0Z') return true
114
- if (raw.startsWith('M500L1500Q200020050L200150Q200200150200')) return true
115
- if (/^M00L([0-9.]+)0L\1([0-9.]+)L0\2Z$/i.test(raw)) return true
116
- if (/^M00H([0-9.]+)V([0-9.]+)H0Z$/i.test(raw)) return true
117
- if ((el.style?.borderRadius && parseFloat(String(el.style.borderRadius)) > 0) || (el.radius && parseFloat(String(el.radius)) > 0)) {
118
- if (raw.includes('L2000L200200L0200') || (raw.startsWith('M00L') && raw.endsWith('Z'))) return true
119
- }
120
- return false
121
- }
122
-
123
- function getRectRadiusInVb(el: any): { rx: number; ry: number } {
124
- if (!el) return { rx: 0, ry: 0 }
125
- const { width, height } = getElementWidthHeight(el)
126
- let r = 0
127
- const isRoundRect = el.pathFormula === 'roundRect' || el.shapType === 'roundRect' || el.title === '圆角矩形'
128
- const rVal = el.style?.borderRadius !== undefined && el.style?.borderRadius !== null && el.style?.borderRadius !== ''
129
- ? el.style.borderRadius
130
- : el.radius
131
-
132
- if (isRoundRect && Array.isArray(el.keypoints) && el.keypoints[0] !== undefined && el.keypoints[0] !== null) {
133
- const kp = Number(el.keypoints[0])
134
- if (!isNaN(kp)) {
135
- r = Math.min(width, height) * Math.max(0, Math.min(0.5, kp))
136
- }
137
- } else if (isRoundRect && el.keypoints && typeof el.keypoints === 'object' && !Array.isArray(el.keypoints) && (el.keypoints as any).adj !== undefined) {
138
- const adj = Number((el.keypoints as any).adj)
139
- if (!isNaN(adj)) {
140
- r = Math.min(width, height) * Math.max(0, Math.min(0.5, adj * 0.5))
141
- }
142
- } else if (rVal !== undefined && rVal !== null && rVal !== '') {
143
- const parsed = parseFloat(String(rVal))
144
- if (!isNaN(parsed)) r = parsed
145
- } else if (isRoundRect) {
146
- r = Math.min(width, height) * 0.125
147
- }
148
-
149
- if (r <= 0) return { rx: 0, ry: 0 }
150
-
151
- const { w: vbW, h: vbH } = getShapeVb(el)
152
-
153
- const maxR = Math.min(width, height) / 2
154
- const clampedR = Math.min(r, maxR)
155
-
156
- const hasBorder = hasElementBorder(el)
157
- const bw = hasBorder ? getEffectiveBorderWidth(el) : 0
158
- const effW = bw > 0 ? Math.max(1, width - bw) : width
159
- const effH = bw > 0 ? Math.max(1, height - bw) : height
160
-
161
- const rx = (clampedR * vbW) / effW
162
- const ry = (clampedR * vbH) / effH
163
-
164
- return { rx, ry }
165
- }
166
-
167
- const props = defineProps<{
168
- element: any
169
- activeSlide?: any
170
- isSelected?: boolean
171
- isEditingText?: boolean
172
- selectedElementId?: string | null
173
- themeRecipe?: any
174
- normalizeHtmlFontFamily?: (html: string) => string
175
- getTextStrokeStyle?: (el: any) => string
176
- getElementTextShadow?: (el: any) => string
177
- getReflectionLeadingCompensation?: (el: any) => number
178
- isImagePlaceholder?: (el: any) => boolean
179
- isReflection?: boolean
180
- // 直接父盒子(组合)的像素尺寸,用于把 % 样式的宽高解析成 px。
181
- // 由 CanvasGroupElement 传入;顶层元素不传,走 px 样式分支。
182
- parentWidthPx?: number
183
- parentHeightPx?: number
184
- }>()
185
-
186
- const emit = defineEmits<{
187
- (e: 'textFocus'): void
188
- (e: 'textBlur', ev: FocusEvent): void
189
- (e: 'textInput', ev: Event): void
190
- (e: 'textKeyDown', ev: KeyboardEvent): void
191
- (e: 'startEditing', ev?: MouseEvent): void
192
- (e: 'placeholderUpload', id: string): void
193
- (e: 'mousemove', ev: MouseEvent): void
194
- (e: 'mouseleave', ev: MouseEvent): void
195
- (e: 'dblclick', ev: MouseEvent): void
196
- }>()
197
-
198
- // 编辑态:安装自绘选区高亮(原生 ::selection 几何跟随 CJK 字体度量,条顶空白过大)
199
- const shapeEditableRef = ref<HTMLElement | null>(null)
200
- const shapeTextHostRef = ref<HTMLElement | null>(null)
201
- let selOverlay: TextSelectionOverlayHandle | null = null
202
-
203
- const isVerticalWriting = computed(() => {
204
- const wm = props.element.style?.writingMode
205
- return wm === 'vertical-rl' || wm === 'vertical-lr'
206
- })
207
-
208
- watch([isTextSelectable, isVerticalWriting], ([selectable]) => {
209
- nextTick(() => {
210
- selOverlay?.destroy()
211
- selOverlay = null
212
- if (selectable && !isVerticalWriting.value) {
213
- const ed = shapeEditableRef.value
214
- const host = shapeTextHostRef.value
215
- if (ed && host) selOverlay = createTextSelectionOverlay(ed, host)
216
- }
217
- })
218
- }, { immediate: true })
219
-
220
- onBeforeUnmount(() => {
221
- selOverlay?.destroy()
222
- selOverlay = null
223
- })
224
-
225
- function getElementWidthHeight(el: any): { width: number; height: number } {
226
- if (!el || typeof el !== 'object') return { width: 200, height: 200 }
227
-
228
- const wStr = el.style?.width !== undefined ? String(el.style.width).trim() : ''
229
- const hStr = el.style?.height !== undefined ? String(el.style.height).trim() : ''
230
-
231
- // 1. 如果有明确的 px 或非百分比数值(如 '120px', 120)
232
- if (wStr && !wStr.endsWith('%') && wStr !== 'auto') {
233
- const w = parseFloat(wStr)
234
- const h = parseFloat(hStr)
235
- if (!isNaN(w) && w > 0) {
236
- return { width: w, height: (!isNaN(h) && h > 0) ? h : (typeof el.height === 'number' && el.height > 0 ? el.height : 200) }
237
- }
238
- }
239
-
240
- // 2. 如果是 % 样式(组合内子元素):相对直接父盒子的像素尺寸解析
241
- if (wStr.endsWith('%')) {
242
- const pct = parseFloat(wStr)
243
- const pW = Number(props.parentWidthPx)
244
- const pH = Number(props.parentHeightPx)
245
-
246
- // 优先:若 props.parentWidthPx 经 props 链传入且有效
247
- if (!isNaN(pct) && isFinite(pW) && pW > 0) {
248
- const width = (pct / 100) * pW
249
- let height = NaN
250
- const hPct = parseFloat(hStr)
251
- if (hStr.endsWith('%') && !isNaN(hPct) && isFinite(pH) && pH > 0) {
252
- height = (hPct / 100) * pH
253
- } else if (hStr && hStr !== 'auto' && !hStr.endsWith('%')) {
254
- const hn = parseFloat(hStr)
255
- if (!isNaN(hn) && hn > 0) height = hn
256
- }
257
- if (isNaN(height) && typeof el.height === 'number' && el.height > 0) height = el.height
258
- if (isNaN(height) && isFinite(pH) && pH > 0) height = pH
259
- if (!isNaN(width) && !isNaN(height)) return { width, height: height || 200 }
260
- }
261
-
262
- // 关键补救:如果 parentWidthPx 尚未就绪,但 el.width / el.height 本身存在确凿数字(组合时保存的原始像素)
263
- if (typeof el.width === 'number' && el.width > 0 && typeof el.height === 'number' && el.height > 0) {
264
- return { width: el.width, height: el.height }
265
- }
266
-
267
- // 回退到画布基准 1280x720 进行百分比换算(绝不直接使用 parseFloat('38.46%') 变成 38px)
268
- if (!isNaN(pct)) {
269
- const width = (pct / 100) * 1280
270
- const hPct = parseFloat(hStr)
271
- const height = (!isNaN(hPct) && hStr.endsWith('%')) ? (hPct / 100) * 720 : (typeof el.height === 'number' && el.height > 0 ? el.height : width)
272
- return { width, height }
273
- }
274
- }
275
-
276
- // 3. 元素自身有数值型 width / height 属性
277
- if (typeof el.width === 'number' && el.width > 0 && typeof el.height === 'number' && el.height > 0) {
278
- return { width: el.width, height: el.height }
279
- }
280
-
281
- // 4. DOM 测量兜底
282
- if (typeof window !== 'undefined' && el.id) {
283
- const dom = document.getElementById(`canvas-el-${el.id}`)
284
- if (dom && dom.offsetWidth > 0 && dom.offsetHeight > 0) {
285
- return { width: dom.offsetWidth, height: dom.offsetHeight }
286
- }
287
- }
288
-
289
- // 5. 最终兜底:如果是 % 严禁直接 parseFloat 当作 px
290
- let fallbackW = 200
291
- let fallbackH = 200
292
- if (wStr && !wStr.endsWith('%') && !isNaN(parseFloat(wStr))) fallbackW = parseFloat(wStr)
293
- if (hStr && !hStr.endsWith('%') && !isNaN(parseFloat(hStr))) fallbackH = parseFloat(hStr)
294
-
295
- return { width: fallbackW, height: fallbackH }
296
- }
297
-
298
- function getBevelShadingPaths(el: any) {
299
- const vW = el.viewBox ? el.viewBox[0] : 200
300
- const vH = el.viewBox ? el.viewBox[1] : 200
301
- const val = el.keypoints?.[0] !== undefined ? el.keypoints[0] : 0.15
302
- const d = Math.min(vW, vH) * val
303
- return {
304
- left: `M 0 0 L ${d} ${d} L ${d} ${vH - d} L 0 ${vH} Z`,
305
- top: `M 0 0 L ${vW} 0 L ${vW - d} ${d} L ${d} ${d} Z`,
306
- bottom: `M 0 ${vH} L ${vW} ${vH} L ${vW - d} ${vH - d} L ${d} ${vH - d} Z`,
307
- right: `M ${vW} 0 L ${vW - d} ${d} L ${vW - d} ${vH - d} L ${vW} ${vH} Z`
308
- }
309
- }
310
-
311
- function getCubeShadingPaths(el: any) {
312
- const vW = el.viewBox ? el.viewBox[0] : 200
313
- const vH = el.viewBox ? el.viewBox[1] : 200
314
- const val = el.keypoints?.[0] !== undefined ? el.keypoints[0] : 0.25
315
- const d = Math.min(vW, vH) * val
316
- return {
317
- top: `M 0 ${d} L ${d} 0 L ${vW} 0 L ${vW - d} ${d} Z`,
318
- right: `M ${vW - d} ${d} L ${vW} 0 L ${vW} ${vH - d} L ${vW - d} ${vH} Z`
319
- }
320
- }
321
-
322
- function getRawText(el: any): string {
323
- let val = ''
324
- if (typeof el.text === 'string') val = el.text
325
- else if (el.text && typeof el.text === 'object') {
326
- val = el.text.content || el.text.text || ''
327
- }
328
- if (isSvgPathString(val)) return ''
329
- return val
330
- }
331
-
332
- function getImagePlaceholderFill(el: any): string {
333
- const fill = getElementFill(el)
334
- if (!fill || fill === '#f1f5f9' || fill === '#f8fafc') {
335
- return '#d9dde3'
336
- }
337
- return fill
338
- }
339
-
340
- function getImagePlaceholderRadius(el: any): string {
341
- const raw = el.style?.borderRadius !== undefined
342
- ? (typeof el.style.borderRadius === 'number' ? `${el.style.borderRadius}px` : el.style.borderRadius)
343
- : (el.radius ? `${el.radius}px` : '0px')
344
- if (!raw || raw === '0px' || raw === '4px') {
345
- return '12px'
346
- }
347
- return raw
348
- }
349
-
350
- function formatShapeHtml(el: any): string {
351
- if (props.isImagePlaceholder && props.isImagePlaceholder(el)) {
352
- return ''
353
- }
354
- const raw = getRawText(el)
355
- const norm = props.normalizeHtmlFontFamily ? props.normalizeHtmlFontFamily(raw) : raw
356
- if (el.style?.textBackground && el.style.textBackground !== 'transparent') {
357
- if (!norm.includes('background-color')) {
358
- return `<span style='background-color: ${el.style.textBackground}; display: inline;'>${norm}</span>`
359
- }
360
- }
361
- return norm
362
- }
363
- </script>
364
-
365
- <template>
366
- <div
367
- class="element-content shape-element-content"
368
- style="width: 100%; height: 100%; position: relative;"
369
- @dblclick.stop.prevent="isReadonly ? null : (isImagePlaceholder && isImagePlaceholder(element) ? emit('placeholderUpload', element.id) : (isTextSelectable ? null : emit('startEditing', $event)))"
370
- >
371
- <!-- Circle Shape (with fill) -->
372
- <div
373
- v-if="(element.content === 'circle' || isEllipseShape(element) || element.shapType === 'ellipse' || element.style?.borderRadius === '50%') && hasElementFill(element)"
374
- :style="{
375
- width: '100%',
376
- height: '100%',
377
- backgroundColor: getElementPattern(element) ? 'transparent' : ((getElementGradientCss(element) || (getElementFill(element) !== undefined && getElementFill(element) !== 'none')) ? (getElementGradientCss(element) ? 'transparent' : getElementFill(element)) : (getElementFill(element) === 'none' ? 'transparent' : '#3b82f6')),
378
- backgroundImage: getElementPattern(element) ? `url('${getElementPattern(element)}')` : (getElementGradientCss(element) || 'none'),
379
- backgroundSize: getElementPattern(element) ? (isElementPatternTile(element) ? `${getElementPatternTileSize(element).w}px ${getElementPatternTileSize(element).h}px` : 'cover') : undefined,
380
- backgroundPosition: getElementPattern(element) ? (isElementPatternTile(element) ? '0 0' : 'center center') : undefined,
381
- backgroundRepeat: getElementPattern(element) ? (isElementPatternTile(element) ? 'repeat' : 'no-repeat') : undefined,
382
- borderRadius: '50%',
383
- borderWidth: (isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true)
384
- ? '0px'
385
- : (element.style?.enableBorder === false ? '0px' : (element.style?.borderWidth || `${getElementBorderWidth(element)}px`)),
386
- borderStyle: element.style?.borderStyle || 'solid',
387
- borderColor: (isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true)
388
- ? 'transparent'
389
- : (element.style?.enableBorder === false ? 'transparent' : (hasElementBorderGradient(element) ? 'transparent' : (element.style?.borderColor || getElementBorderColor(element) || 'transparent'))),
390
- borderImage: hasElementBorderGradient(element) ? `${getElementBorderGradientCss(element)} 1` : undefined,
391
- filter: (element.shadow && element.shadow.type === 'inner') ? `url(#shape-inner-shadow-${element.id})` : ''
392
- }"
393
- ></div>
394
-
395
- <!-- Circle Shape (without fill) -->
396
- <div
397
- v-else-if="element.content === 'circle' || isEllipseShape(element) || element.shapType === 'ellipse' || element.style?.borderRadius === '50%'"
398
- style="width: 100%; height: 100%; overflow: visible;"
399
- >
400
- <svg
401
- viewBox="0 0 100 100"
402
- preserveAspectRatio="none"
403
- style="width: 100%; height: 100%; display: block; overflow: visible;"
404
- :style="{ filter: (element.shadow && element.shadow.type === 'inner') ? `url(#shape-inner-shadow-${element.id})` : '' }"
405
- >
406
- <defs v-if="getElementBorderGradientConfig(element)">
407
- <radialGradient
408
- v-if="getElementBorderGradientConfig(element)!.path && getElementBorderGradientConfig(element)!.path !== 'line'"
409
- :id="`shape-border-gradient-${svgIdKey}`"
410
- :cx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
411
- :cy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
412
- :r="getElementBorderGradientConfig(element)!.path === 'rect' ? '60%' : (getElementBorderGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
413
- :fx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
414
- :fy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
415
- >
416
- <stop
417
- v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
418
- :key="idx"
419
- :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
420
- :stop-color="getHexColorWithoutAlpha(c.color)"
421
- :stop-opacity="getAlphaFromColor(c.color)"
422
- />
423
- </radialGradient>
424
- <linearGradient
425
- v-else
426
- :id="`shape-border-gradient-${svgIdKey}`"
427
- :x1="getSvgGradientCoords(getElementBorderGradientConfig(element)).x1"
428
- :y1="getSvgGradientCoords(getElementBorderGradientConfig(element)).y1"
429
- :x2="getSvgGradientCoords(getElementBorderGradientConfig(element)).x2"
430
- :y2="getSvgGradientCoords(getElementBorderGradientConfig(element)).y2"
431
- >
432
- <stop
433
- v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
434
- :key="idx"
435
- :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
436
- :stop-color="getHexColorWithoutAlpha(c.color)"
437
- :stop-opacity="getAlphaFromColor(c.color)"
438
- />
439
- </linearGradient>
440
- </defs>
441
- <ellipse
442
- cx="50"
443
- cy="50"
444
- rx="50"
445
- ry="50"
446
- fill="none"
447
- :stroke="getEffectiveBorderColor(element, '#2563eb')"
448
- :stroke-width="getEffectiveBorderWidth(element)"
449
- :stroke-dasharray="element.style?.borderStyle === 'dashed' || element.borderType === 'dashed' ? '5 5' : (element.style?.borderStyle === 'dotted' ? '2 2' : '')"
450
- vector-effect="non-scaling-stroke"
451
- />
452
- </svg>
453
- </div>
454
-
455
- <!-- Triangle Shape -->
456
- <div v-else-if="element.content === 'triangle'" style="width: 100%; height: 100%; overflow: visible;">
457
- <svg
458
- viewBox="0 0 100 100"
459
- preserveAspectRatio="none"
460
- style="width: 100%; height: 100%; display: block; overflow: visible;"
461
- :style="{ filter: (element.shadow && element.shadow.type === 'inner') ? `url(#shape-inner-shadow-${element.id})` : '' }"
462
- >
463
- <defs>
464
- <pattern
465
- v-if="getElementPattern(element)"
466
- :id="`shape-pattern-${svgIdKey}`"
467
- :width="isElementPatternTile(element) ? getElementPatternTileSize(element).w : '100%'"
468
- :height="isElementPatternTile(element) ? getElementPatternTileSize(element).h : '100%'"
469
- :patternUnits="isElementPatternTile(element) ? 'userSpaceOnUse' : 'objectBoundingBox'"
470
- :patternContentUnits="isElementPatternTile(element) ? 'userSpaceOnUse' : 'objectBoundingBox'"
471
- :patternTransform="getShapePatternTransform(element, true)"
472
- >
473
- <image
474
- :href="getElementPattern(element)"
475
- :width="isElementPatternTile(element) ? getElementPatternTileSize(element).w : 1"
476
- :height="isElementPatternTile(element) ? getElementPatternTileSize(element).h : 1"
477
- preserveAspectRatio="none"
478
- />
479
- </pattern>
480
- <template v-if="getElementGradientConfig(element)">
481
- <radialGradient
482
- v-if="getElementGradientConfig(element)!.path && getElementGradientConfig(element)!.path !== 'line'"
483
- :id="`shape-gradient-${svgIdKey}`"
484
- :cx="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.x * 100) : 50}%`"
485
- :cy="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.y * 100) : 50}%`"
486
- :r="getElementGradientConfig(element)!.path === 'rect' ? '60%' : (getElementGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
487
- :fx="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.x * 100) : 50}%`"
488
- :fy="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.y * 100) : 50}%`"
489
- >
490
- <stop
491
- v-for="(c, idx) in (getElementGradientConfig(element)?.colors || [])"
492
- :key="idx"
493
- :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
494
- :stop-color="getHexColorWithoutAlpha(c.color)"
495
- :stop-opacity="getAlphaFromColor(c.color)"
496
- />
497
- </radialGradient>
498
- <linearGradient
499
- v-else
500
- :id="`shape-gradient-${svgIdKey}`"
501
- :x1="getSvgGradientCoords(element).x1"
502
- :y1="getSvgGradientCoords(element).y1"
503
- :x2="getSvgGradientCoords(element).x2"
504
- :y2="getSvgGradientCoords(element).y2"
505
- >
506
- <stop
507
- v-for="(c, idx) in (getElementGradientConfig(element)?.colors || [])"
508
- :key="idx"
509
- :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
510
- :stop-color="getHexColorWithoutAlpha(c.color)"
511
- :stop-opacity="getAlphaFromColor(c.color)"
512
- />
513
- </linearGradient>
514
- </template>
515
- <template v-if="getElementBorderGradientConfig(element)">
516
- <radialGradient
517
- v-if="getElementBorderGradientConfig(element)!.path && getElementBorderGradientConfig(element)!.path !== 'line'"
518
- :id="`shape-border-gradient-${svgIdKey}`"
519
- :cx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
520
- :cy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
521
- :r="getElementBorderGradientConfig(element)!.path === 'rect' ? '60%' : (getElementBorderGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
522
- :fx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
523
- :fy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
524
- >
525
- <stop
526
- v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
527
- :key="idx"
528
- :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
529
- :stop-color="getHexColorWithoutAlpha(c.color)"
530
- :stop-opacity="getAlphaFromColor(c.color)"
531
- />
532
- </radialGradient>
533
- <linearGradient
534
- v-else
535
- :id="`shape-border-gradient-${svgIdKey}`"
536
- :x1="getSvgGradientCoords(getElementBorderGradientConfig(element)).x1"
537
- :y1="getSvgGradientCoords(getElementBorderGradientConfig(element)).y1"
538
- :x2="getSvgGradientCoords(getElementBorderGradientConfig(element)).x2"
539
- :y2="getSvgGradientCoords(getElementBorderGradientConfig(element)).y2"
540
- >
541
- <stop
542
- v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
543
- :key="idx"
544
- :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
545
- :stop-color="getHexColorWithoutAlpha(c.color)"
546
- :stop-opacity="getAlphaFromColor(c.color)"
547
- />
548
- </linearGradient>
549
- </template>
550
- </defs>
551
- <polygon
552
- points="50,0 0,100 100,100"
553
- :fill="hasElementFill(element) ? (getElementPattern(element) ? `url(#shape-pattern-${svgIdKey})` : (getElementGradientConfig(element) ? `url(#shape-gradient-${svgIdKey})` : (element.style?.fill !== undefined ? element.style.fill : '#f59e0b'))) : 'none'"
554
- :stroke="getEffectiveBorderColor(element, '#d97706')"
555
- :stroke-width="getEffectiveBorderWidth(element)"
556
- :stroke-dasharray="element.style?.borderStyle === 'dashed' ? '5 5' : ''"
557
- />
558
- </svg>
559
- </div>
560
-
561
- <!-- SVG Path Shapes -->
562
- <div
563
- v-else-if="(element.path || element.content) && ((element.path || element.content).trim().startsWith('M') || (element.path || element.content).trim().startsWith('m'))"
564
- style="width: 100%; height: 100%; overflow: visible;"
565
- >
566
- <svg
567
- overflow="visible"
568
- style="width: 100%; height: 100%; display: block; overflow: visible;"
569
- :style="{ filter: (element.shadow && element.shadow.type === 'inner') ? `url(#shape-inner-shadow-${element.id})` : '' }"
570
- >
571
- <defs>
572
- <pattern
573
- v-if="getElementPattern(element)"
574
- :id="`shape-pattern-${svgIdKey}`"
575
- :width="isElementPatternTile(element) ? getElementPatternTileSize(element).w : '100%'"
576
- :height="isElementPatternTile(element) ? getElementPatternTileSize(element).h : '100%'"
577
- :patternUnits="isElementPatternTile(element) ? 'userSpaceOnUse' : 'objectBoundingBox'"
578
- :patternContentUnits="isElementPatternTile(element) ? 'userSpaceOnUse' : 'objectBoundingBox'"
579
- :patternTransform="getShapePatternTransform(element, false)"
580
- >
581
- <image
582
- :href="getElementPattern(element)"
583
- :width="isElementPatternTile(element) ? getElementPatternTileSize(element).w : 1"
584
- :height="isElementPatternTile(element) ? getElementPatternTileSize(element).h : 1"
585
- preserveAspectRatio="none"
586
- />
587
- </pattern>
588
- <template v-if="getElementGradientConfig(element)">
589
- <radialGradient
590
- v-if="getElementGradientConfig(element)!.path && getElementGradientConfig(element)!.path !== 'line'"
591
- :id="`shape-gradient-${svgIdKey}`"
592
- :cx="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.x * 100) : 50}%`"
593
- :cy="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.y * 100) : 50}%`"
594
- :r="getElementGradientConfig(element)!.path === 'rect' ? '60%' : (getElementGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
595
- :fx="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.x * 100) : 50}%`"
596
- :fy="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.y * 100) : 50}%`"
597
- >
598
- <stop
599
- v-for="(c, idx) in (getElementGradientConfig(element)?.colors || [])"
600
- :key="idx"
601
- :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
602
- :stop-color="getHexColorWithoutAlpha(c.color)"
603
- :stop-opacity="getAlphaFromColor(c.color)"
604
- />
605
- </radialGradient>
606
- <linearGradient
607
- v-else
608
- :id="`shape-gradient-${svgIdKey}`"
609
- :x1="getSvgGradientCoords(element).x1"
610
- :y1="getSvgGradientCoords(element).y1"
611
- :x2="getSvgGradientCoords(element).x2"
612
- :y2="getSvgGradientCoords(element).y2"
613
- >
614
- <stop
615
- v-for="(c, idx) in (getElementGradientConfig(element)?.colors || [])"
616
- :key="idx"
617
- :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
618
- :stop-color="getHexColorWithoutAlpha(c.color)"
619
- :stop-opacity="getAlphaFromColor(c.color)"
620
- />
621
- </linearGradient>
622
- </template>
623
- <template v-if="getElementBorderGradientConfig(element)">
624
- <radialGradient
625
- v-if="getElementBorderGradientConfig(element)!.path && getElementBorderGradientConfig(element)!.path !== 'line'"
626
- :id="`shape-border-gradient-${svgIdKey}`"
627
- :cx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
628
- :cy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
629
- :r="getElementBorderGradientConfig(element)!.path === 'rect' ? '60%' : (getElementBorderGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
630
- :fx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
631
- :fy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
632
- >
633
- <stop
634
- v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
635
- :key="idx"
636
- :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
637
- :stop-color="getHexColorWithoutAlpha(c.color)"
638
- :stop-opacity="getAlphaFromColor(c.color)"
639
- />
640
- </radialGradient>
641
- <linearGradient
642
- v-else
643
- :id="`shape-border-gradient-${svgIdKey}`"
644
- :x1="getSvgGradientCoords(getElementBorderGradientConfig(element)).x1"
645
- :y1="getSvgGradientCoords(getElementBorderGradientConfig(element)).y1"
646
- :x2="getSvgGradientCoords(getElementBorderGradientConfig(element)).x2"
647
- :y2="getSvgGradientCoords(getElementBorderGradientConfig(element)).y2"
648
- >
649
- <stop
650
- v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
651
- :key="idx"
652
- :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
653
- :stop-color="getHexColorWithoutAlpha(c.color)"
654
- :stop-opacity="getAlphaFromColor(c.color)"
655
- />
656
- </linearGradient>
657
- </template>
658
- </defs>
659
- <g
660
- :transform="getShapeSvgTransform(element)"
661
- >
662
- <rect
663
- v-if="isRectShape(element)"
664
- x="0"
665
- y="0"
666
- :width="getShapeVb(element).w"
667
- :height="getShapeVb(element).h"
668
- :rx="getRectRadiusInVb(element).rx"
669
- :ry="getRectRadiusInVb(element).ry"
670
- vector-effect="non-scaling-stroke"
671
- :fill="hasElementFill(element) ? (getElementPattern(element) ? `url(#shape-pattern-${svgIdKey})` : (getElementGradientConfig(element) ? `url(#shape-gradient-${svgIdKey})` : (getElementFill(element) !== undefined ? getElementFill(element) : '#3b82f6'))) : 'none'"
672
- :stroke="(isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true) ? 'none' : (element.style?.enableBorder === false ? 'none' : getEffectiveBorderColor(element, 'none'))"
673
- :stroke-width="(isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true) ? 0 : (element.style?.enableBorder === false ? 0 : getElementBorderWidth(element))"
674
- :stroke-linecap="element.style?.strokeLinecap || element.borderLineCap || element.lineCap || (element.shapType === 'arc' ? 'round' : (isOpenLineShape(element) ? 'butt' : undefined))"
675
- :stroke-dasharray="element.style?.borderStyle === 'dashed' || element.borderType === 'dashed' ? '5 5' : ''"
676
- />
677
- <path
678
- v-else
679
- vector-effect="non-scaling-stroke"
680
- :d="element.path || element.content"
681
- :fill="hasElementFill(element) ? (getElementPattern(element) ? `url(#shape-pattern-${svgIdKey})` : (getElementGradientConfig(element) ? `url(#shape-gradient-${svgIdKey})` : (getElementFill(element) !== undefined ? getElementFill(element) : '#3b82f6'))) : 'none'"
682
- :stroke="(isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true) ? 'none' : (element.style?.enableBorder === false ? 'none' : getEffectiveBorderColor(element, 'none'))"
683
- :stroke-width="(isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true) ? 0 : (element.style?.enableBorder === false ? 0 : getElementBorderWidth(element))"
684
- :stroke-linecap="element.style?.strokeLinecap || element.borderLineCap || element.lineCap || (element.shapType === 'arc' ? 'round' : (isOpenLineShape(element) ? 'butt' : undefined))"
685
- :stroke-dasharray="element.style?.borderStyle === 'dashed' || element.borderType === 'dashed' ? '5 5' : ''"
686
- />
687
- <template v-if="element.pathFormula === 'bevel'">
688
- <path :d="getBevelShadingPaths(element).left" fill="white" fill-opacity="0.2" stroke="none" pointer-events="none" />
689
- <path :d="getBevelShadingPaths(element).top" fill="black" fill-opacity="0.08" stroke="none" pointer-events="none" />
690
- <path :d="getBevelShadingPaths(element).bottom" fill="black" fill-opacity="0.15" stroke="none" pointer-events="none" />
691
- <path :d="getBevelShadingPaths(element).right" fill="black" fill-opacity="0.25" stroke="none" pointer-events="none" />
692
- </template>
693
- <template v-if="element.pathFormula === 'cube'">
694
- <path :d="getCubeShadingPaths(element).top" fill="white" fill-opacity="0.15" stroke="none" pointer-events="none" />
695
- <path :d="getCubeShadingPaths(element).right" fill="black" fill-opacity="0.2" stroke="none" pointer-events="none" />
696
- </template>
697
- </g>
698
- </svg>
699
- </div>
700
-
701
- <!-- Default Rectangle Shape (with fill) -->
702
- <div
703
- v-else-if="hasElementFill(element)"
704
- :style="{
705
- width: '100%',
706
- height: '100%',
707
- backgroundColor: getElementPattern(element) ? 'transparent' : ((isImagePlaceholder && isImagePlaceholder(element) && !getElementGradientCss(element)) ? getImagePlaceholderFill(element) : ((getElementGradientCss(element) || (getElementFill(element) !== undefined && getElementFill(element) !== 'none')) ? (getElementGradientCss(element) ? 'transparent' : getElementFill(element)) : (getElementFill(element) === 'none' ? 'transparent' : '#10b981'))),
708
- backgroundImage: getElementPattern(element) ? `url('${getElementPattern(element)}')` : (getElementGradientCss(element) || 'none'),
709
- backgroundSize: getElementPattern(element) ? (isElementPatternTile(element) ? `${getElementPatternTileSize(element).w}px ${getElementPatternTileSize(element).h}px` : 'cover') : undefined,
710
- backgroundPosition: getElementPattern(element) ? (isElementPatternTile(element) ? '0 0' : 'center center') : undefined,
711
- backgroundRepeat: getElementPattern(element) ? (isElementPatternTile(element) ? 'repeat' : 'no-repeat') : undefined,
712
- borderRadius: (isImagePlaceholder && isImagePlaceholder(element)) ? getImagePlaceholderRadius(element) : (element.style?.borderRadius !== undefined ? (typeof element.style.borderRadius === 'number' ? `${element.style.borderRadius}px` : element.style.borderRadius) : (element.radius ? `${element.radius}px` : '0px')),
713
- borderWidth: (isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true)
714
- ? '0px'
715
- : (element.style?.enableBorder === false ? '0px' : `${getElementBorderWidth(element)}px`),
716
- borderStyle: element.style?.borderStyle || element.borderType || 'solid',
717
- borderColor: (isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true)
718
- ? 'transparent'
719
- : (element.style?.enableBorder === false ? 'transparent' : (hasElementBorderGradient(element) ? 'transparent' : (element.style?.borderColor || getElementBorderColor(element) || 'transparent'))),
720
- borderImage: hasElementBorderGradient(element) ? `${getElementBorderGradientCss(element)} 1` : undefined,
721
- filter: (element.shadow && element.shadow.type === 'inner') ? `url(#shape-inner-shadow-${element.id})` : ''
722
- }"
723
- ></div>
724
-
725
- <!-- Default Rectangle Shape (without fill) -->
726
- <div
727
- v-else
728
- style="width: 100%; height: 100%; overflow: visible;"
729
- >
730
- <svg
731
- style="width: 100%; height: 100%; display: block; overflow: visible;"
732
- :style="{ filter: (element.shadow && element.shadow.type === 'inner') ? `url(#shape-inner-shadow-${element.id})` : '' }"
733
- >
734
- <defs v-if="getElementBorderGradientConfig(element)">
735
- <radialGradient
736
- v-if="getElementBorderGradientConfig(element)!.path && getElementBorderGradientConfig(element)!.path !== 'line'"
737
- :id="`shape-border-gradient-${svgIdKey}`"
738
- :cx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
739
- :cy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
740
- :r="getElementBorderGradientConfig(element)!.path === 'rect' ? '60%' : (getElementBorderGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
741
- :fx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
742
- :fy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
743
- >
744
- <stop
745
- v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
746
- :key="idx"
747
- :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
748
- :stop-color="getHexColorWithoutAlpha(c.color)"
749
- :stop-opacity="getAlphaFromColor(c.color)"
750
- />
751
- </radialGradient>
752
- <linearGradient
753
- v-else
754
- :id="`shape-border-gradient-${svgIdKey}`"
755
- :x1="getSvgGradientCoords(getElementBorderGradientConfig(element)).x1"
756
- :y1="getSvgGradientCoords(getElementBorderGradientConfig(element)).y1"
757
- :x2="getSvgGradientCoords(getElementBorderGradientConfig(element)).x2"
758
- :y2="getSvgGradientCoords(getElementBorderGradientConfig(element)).y2"
759
- >
760
- <stop
761
- v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
762
- :key="idx"
763
- :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
764
- :stop-color="getHexColorWithoutAlpha(c.color)"
765
- :stop-opacity="getAlphaFromColor(c.color)"
766
- />
767
- </linearGradient>
768
- </defs>
769
- <rect
770
- x="0"
771
- y="0"
772
- width="100%"
773
- height="100%"
774
- :rx="parseFloat(element.style?.borderRadius) || parseFloat(element.radius) || 0"
775
- :ry="parseFloat(element.style?.borderRadius) || parseFloat(element.radius) || 0"
776
- fill="none"
777
- :stroke="getEffectiveBorderColor(element, '#10b981')"
778
- :stroke-width="getEffectiveBorderWidth(element)"
779
- :stroke-dasharray="element.style?.borderStyle === 'dashed' || element.borderType === 'dashed' ? '5 5' : (element.style?.borderStyle === 'dotted' ? '2 2' : '')"
780
- vector-effect="non-scaling-stroke"
781
- />
782
- </svg>
783
- </div>
784
-
785
- <!-- Image Placeholder Centered Icon Overlay -->
786
- <div
787
- v-if="isImagePlaceholder && isImagePlaceholder(element)"
788
- class="image-placeholder-icon-overlay"
789
- style="position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; pointer-events: none;"
790
- >
791
- <svg width="56" height="56" viewBox="0 0 24 24" fill="none" style="max-width: 45%; max-height: 45%;">
792
- <rect x="3" y="3" width="18" height="18" rx="2" stroke="#475569" stroke-width="2" />
793
- <circle cx="8.5" cy="8.5" r="1.5" fill="#475569" />
794
- <path d="M21 15l-5-5L5 21" stroke="#475569" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
795
- </svg>
796
- </div>
797
-
798
- <!-- Embedded Shape Text Overlay -->
799
- <div
800
- v-else
801
- class="shape-text-overlay-wrapper"
802
- style="position: absolute; display: flex; flex-direction: column; box-sizing: border-box; pointer-events: none;"
803
- :style="{
804
- left: isEllipseShape(element) ? '14.6%' : '0px',
805
- top: isEllipseShape(element) ? '14.6%' : '0px',
806
- width: isEllipseShape(element) ? '70.8%' : '100%',
807
- height: isEllipseShape(element) ? '70.8%' : '100%',
808
- justifyContent: (element.style?.verticalAlign === 'top' || element.style?.verticalAlign === 'up') ? 'flex-start' : ((element.style?.verticalAlign === 'bottom' || element.style?.verticalAlign === 'down') ? 'flex-end' : 'center'),
809
- padding: isEllipseShape(element) ? '0px' : (element.style?.padding !== undefined ? element.style.padding : '4.8px 9.6px 4.8px 9.6px'),
810
- transform: ((element.style as any)?.flipH || element.flipH || element.isFlipH || (element.style as any)?.flipV || element.flipV || element.isFlipV) ? `scale(${((element.style as any)?.flipH || element.flipH || element.isFlipH) ? -1 : 1}, ${((element.style as any)?.flipV || element.flipV || element.isFlipV) ? -1 : 1})` : undefined
811
- }"
812
- >
813
- <div ref="shapeTextHostRef" class="text-reflection-wrapper" style="position: relative; pointer-events: none;" :style="{
814
- width: (element.style?.writingMode === 'vertical-rl' || element.style?.writingMode === 'vertical-lr') ? 'auto' : '100%',
815
- height: (element.style?.writingMode === 'vertical-rl' || element.style?.writingMode === 'vertical-lr') ? '100%' : 'auto'
816
- }">
817
- <div
818
- ref="shapeEditableRef"
819
- :id="isReflection ? undefined : `editable-${element.id}`"
820
- :contenteditable="isTextSelectable ? 'true' : 'false'"
821
- @mousedown="isTextSelectable ? $event.stopPropagation() : null"
822
- @click="isTextSelectable ? $event.stopPropagation() : null"
823
- @focusin="emit('textFocus')"
824
- @focusout="emit('textBlur', $event)"
825
- @input="emit('textInput', $event)"
826
- @keydown="emit('textKeyDown', $event)"
827
- @mousemove="emit('mousemove', $event)"
828
- @mouseleave="emit('mouseleave', $event)"
829
- @dblclick.stop.prevent="isReadonly ? null : (isImagePlaceholder && isImagePlaceholder(element) ? emit('placeholderUpload', element.id) : (isTextSelectable ? null : emit('startEditing', $event)))"
830
- class="shape-text-overlay"
831
- :style="{
832
- display: 'block',
833
- writingMode: element.style?.writingMode || 'horizontal-tb',
834
- width: (element.style?.writingMode === 'vertical-rl' || element.style?.writingMode === 'vertical-lr') ? 'auto' : '100%',
835
- height: (element.style?.writingMode === 'vertical-rl' || element.style?.writingMode === 'vertical-lr') ? '100%' : 'auto',
836
- zoom: (element.autoFit?.type === 'text' && element.autoFit?.fontScale) ? (element.autoFit.fontScale / 100) : 1,
837
- transform: 'none',
838
- maxWidth: '100%',
839
- outline: 'none',
840
- padding: '0px',
841
- boxSizing: 'content-box',
842
- overflow: 'wrap',
843
- wordBreak: 'break-word',
844
- pointerEvents: isReflection ? 'none' : (isTextSelectable ? 'auto' : (getRawText(element).trim() ? 'auto' : 'none')),
845
- cursor: isTextSelectable ? 'text' : undefined,
846
- userSelect: isTextSelectable ? 'text' : 'none',
847
- color: element.style?.textColor || element.style?.color || element.textColor || ((isImagePlaceholder && isImagePlaceholder(element)) ? '#6366f1' : (element.style?.fill === '#ffffff' || element.style?.fill === '#FFFFFF' ? '#475569' : (themeRecipe?.shape?.textColor || '#ffffff'))),
848
- fontFamily: (element.style?.fontFamily && element.style?.fontFamily !== '默认字体' && element.style?.fontFamily !== 'inherit')
849
- ? element.style.fontFamily
850
- : ((element.fontFamily && element.fontFamily !== '默认字体' && element.fontFamily !== 'inherit')
851
- ? element.fontFamily
852
- : (themeRecipe?.styleFonts?.bodyFont || 'SourceHanSansCN')),
853
- fontSize: element.style?.fontSize || ((isImagePlaceholder && isImagePlaceholder(element)) ? '15px' : '16px'),
854
- fontWeight: element.style?.fontWeight || ((isImagePlaceholder && isImagePlaceholder(element)) ? '600' : (getFontVariantStyle(element.style?.fontFamily || element.fontFamily, element.style?.fontFamilyStyle).fontWeight !== 'normal' ? getFontVariantStyle(element.style?.fontFamily || element.fontFamily, element.style?.fontFamilyStyle).fontWeight : 'normal')),
855
- fontStyle: element.style?.fontStyle || getFontVariantStyle(element.style?.fontFamily || element.fontFamily, element.style?.fontFamilyStyle).fontStyle,
856
- textDecoration: element.style?.textDecoration || 'none',
857
- lineHeight: element.style?.lineHeight || 1.35,
858
- textAlign: (element.style?.textAlign === 'distribute' ? 'justify' : (element.style?.textAlign || 'center')) as any,
859
- textAlignLast: (element.style?.textAlign === 'distribute' ? 'justify' : 'auto') as any,
860
- textJustify: (element.style?.textAlign === 'distribute' ? 'inter-character' : 'auto') as any,
861
- whiteSpace: element.style?.whiteSpace || 'normal',
862
- ['-webkit-text-stroke']: getTextStrokeStyle ? getTextStrokeStyle(element) : 'none',
863
- ['paint-order']: element.style?.enableTextStroke ? 'stroke fill' : 'normal',
864
- ['text-shadow']: getElementTextShadow ? getElementTextShadow(element) : 'none',
865
- '--paragraphSpace': `${element.style?.paragraphSpace === undefined ? 3 : element.style.paragraphSpace}px`
866
- }"
867
- v-html="formatShapeHtml(element)"
868
- ></div>
869
- </div>
870
- </div>
871
- </div>
872
- </template>
873
-
874
- <style scoped>
875
- .shape-element-content {
876
- user-select: none;
877
- }
878
- .shape-text-overlay {
879
- user-select: text;
880
- -webkit-user-select: text;
881
- }
882
- </style>
1
+ <script setup lang="ts">
2
+ import { computed, ref, watch, onBeforeUnmount, nextTick } from 'vue'
3
+ import {
4
+ getElementPattern,
5
+ isElementPatternTile,
6
+ getElementPatternTileSize,
7
+ getElementGradientConfig,
8
+ getElementGradientCss,
9
+ getSvgGradientCoords,
10
+ getElementFill,
11
+ hasElementFill,
12
+ hasElementBorder,
13
+ getElementBorderColor,
14
+ getElementBorderWidth,
15
+ getElementBorderGradientConfig,
16
+ hasElementBorderGradient,
17
+ getElementBorderGradientCss,
18
+ isSvgPathString,
19
+ getHexColorWithoutAlpha,
20
+ getAlphaFromColor
21
+ } from '../../../utils/canvas/elementStyles'
22
+ import { isEllipseShape } from '../../../utils/line'
23
+ import { getFontVariantStyle } from '../../../configs/fonts'
24
+ import { useSlides } from '../../../composables/useSlides'
25
+ import { createTextSelectionOverlay, type TextSelectionOverlayHandle } from '../textSelectionOverlay'
26
+
27
+ const { shapeFormatPainter, isReadonly } = useSlides()
28
+ const isTextPaintActive = computed(() => !!(shapeFormatPainter.value && shapeFormatPainter.value.mode === 'text-range'))
29
+ const svgIdKey = computed(() => props.isReflection ? `${props.element.id}-refl` : `${props.element.id}`)
30
+ const isTextSelectable = computed(() => !props.isReflection && ((props.isEditingText && props.selectedElementId === props.element.id) || isTextPaintActive.value))
31
+
32
+ function getEffectiveBorderWidth(el: any): number {
33
+ if (props.isImagePlaceholder && props.isImagePlaceholder(el) && el.style?.enableBorder !== true) return 0
34
+ if (el.style?.enableBorder === false) return 0
35
+ const w = getElementBorderWidth(el)
36
+ if (w > 0) return w
37
+ if (el.style?.borderColor || el.borderColor) return 1
38
+ return 0
39
+ }
40
+
41
+ function getShapePatternTransform(el: any, isTriangle = false): string | undefined {
42
+ if (!el || !isElementPatternTile(el)) return undefined
43
+ const { width, height } = getElementWidthHeight(el)
44
+ if (isTriangle) {
45
+ const sx = width > 0 ? 100 / width : 1
46
+ const sy = height > 0 ? 100 / height : 1
47
+ return `scale(${sx}, ${sy})`
48
+ }
49
+ const vbW = el.viewBox ? el.viewBox[0] : (width || 200)
50
+ const vbH = el.viewBox ? el.viewBox[1] : (height || 200)
51
+ const hasBorder = hasElementBorder(el)
52
+ const bw = hasBorder ? getEffectiveBorderWidth(el) : 0
53
+ const effW = bw > 0 ? Math.max(1, width - bw) : Math.max(1, width)
54
+ const effH = bw > 0 ? Math.max(1, height - bw) : Math.max(1, height)
55
+ return `scale(${vbW / effW}, ${vbH / effH})`
56
+ }
57
+
58
+ function getShapeSvgTransform(el: any): string | undefined {
59
+ if (!el) return undefined
60
+ const { width, height } = getElementWidthHeight(el)
61
+ const vbW = el.viewBox ? el.viewBox[0] : (width || 200)
62
+ const vbH = el.viewBox ? el.viewBox[1] : (height || 200)
63
+
64
+ if (el.shapType === 'arc') {
65
+ return `scale(${width / vbW}, ${height / vbH})`
66
+ }
67
+ if (isOpenLineShape(el)) {
68
+ return undefined
69
+ }
70
+
71
+ const hasBorder = hasElementBorder(el)
72
+ const bw = hasBorder ? getEffectiveBorderWidth(el) : 0
73
+
74
+ if (bw > 0) {
75
+ const scaleX = Math.max(0, width - bw) / vbW
76
+ const scaleY = Math.max(0, height - bw) / vbH
77
+ return `translate(${bw / 2}, ${bw / 2}) scale(${scaleX}, ${scaleY})`
78
+ }
79
+
80
+ const scaleX = width / vbW
81
+ const scaleY = height / vbH
82
+ return `scale(${scaleX}, ${scaleY})`
83
+ }
84
+
85
+ function getEffectiveBorderColor(el: any, fallback = '#10b981'): string {
86
+ if (props.isImagePlaceholder && props.isImagePlaceholder(el) && el.style?.enableBorder !== true) return 'transparent'
87
+ if (el.style?.enableBorder === false) return 'transparent'
88
+ if (hasElementBorderGradient(el)) return `url(#shape-border-gradient-${svgIdKey.value})`
89
+ return el.style?.borderColor || getElementBorderColor(el) || fallback
90
+ }
91
+
92
+ function isOpenLineShape(el: any): boolean {
93
+ if (!el) return false
94
+ const shapType = el.shapType || el.type
95
+ return shapType === 'arc' || shapType === 'straightConnector1' || shapType === 'line' || (typeof el.width === 'number' && el.width <= 0) || (typeof el.height === 'number' && el.height <= 0)
96
+ }
97
+
98
+ function getShapeVb(el: any): { w: number; h: number } {
99
+ if (!el) return { w: 200, h: 200 }
100
+ const { width, height } = getElementWidthHeight(el)
101
+ const w = el.viewBox ? el.viewBox[0] : (width || 200)
102
+ const h = el.viewBox ? el.viewBox[1] : (height || 200)
103
+ return { w, h }
104
+ }
105
+
106
+ function isRectShape(el: any): boolean {
107
+ if (!el) return false
108
+ if (el.shapType === 'rect' || el.shapType === 'roundRect') return true
109
+ if (el.content === 'rect') return true
110
+ if (el.pathFormula === 'roundRect') return true
111
+ if (el.title === '矩形' || el.title === '圆角矩形') return true
112
+ const raw = (el.path || el.content || '').trim().replace(/\s+/g, '').toUpperCase()
113
+ if (raw === 'M00L2000L200200L0200Z' || raw === 'M00H200V200H0Z') return true
114
+ if (raw.startsWith('M500L1500Q200020050L200150Q200200150200')) return true
115
+ if (/^M00L([0-9.]+)0L\1([0-9.]+)L0\2Z$/i.test(raw)) return true
116
+ if (/^M00H([0-9.]+)V([0-9.]+)H0Z$/i.test(raw)) return true
117
+ if ((el.style?.borderRadius && parseFloat(String(el.style.borderRadius)) > 0) || (el.radius && parseFloat(String(el.radius)) > 0)) {
118
+ if (raw.includes('L2000L200200L0200') || (raw.startsWith('M00L') && raw.endsWith('Z'))) return true
119
+ }
120
+ return false
121
+ }
122
+
123
+ function getRectRadiusInVb(el: any): { rx: number; ry: number } {
124
+ if (!el) return { rx: 0, ry: 0 }
125
+ const { width, height } = getElementWidthHeight(el)
126
+ let r = 0
127
+ const isRoundRect = el.pathFormula === 'roundRect' || el.shapType === 'roundRect' || el.title === '圆角矩形'
128
+ const rVal = el.style?.borderRadius !== undefined && el.style?.borderRadius !== null && el.style?.borderRadius !== ''
129
+ ? el.style.borderRadius
130
+ : el.radius
131
+
132
+ if (isRoundRect && Array.isArray(el.keypoints) && el.keypoints[0] !== undefined && el.keypoints[0] !== null) {
133
+ const kp = Number(el.keypoints[0])
134
+ if (!isNaN(kp)) {
135
+ r = Math.min(width, height) * Math.max(0, Math.min(0.5, kp))
136
+ }
137
+ } else if (isRoundRect && el.keypoints && typeof el.keypoints === 'object' && !Array.isArray(el.keypoints) && (el.keypoints as any).adj !== undefined) {
138
+ const adj = Number((el.keypoints as any).adj)
139
+ if (!isNaN(adj)) {
140
+ r = Math.min(width, height) * Math.max(0, Math.min(0.5, adj * 0.5))
141
+ }
142
+ } else if (rVal !== undefined && rVal !== null && rVal !== '') {
143
+ const parsed = parseFloat(String(rVal))
144
+ if (!isNaN(parsed)) r = parsed
145
+ } else if (isRoundRect) {
146
+ r = Math.min(width, height) * 0.125
147
+ }
148
+
149
+ if (r <= 0) return { rx: 0, ry: 0 }
150
+
151
+ const { w: vbW, h: vbH } = getShapeVb(el)
152
+
153
+ const maxR = Math.min(width, height) / 2
154
+ const clampedR = Math.min(r, maxR)
155
+
156
+ const hasBorder = hasElementBorder(el)
157
+ const bw = hasBorder ? getEffectiveBorderWidth(el) : 0
158
+ const effW = bw > 0 ? Math.max(1, width - bw) : width
159
+ const effH = bw > 0 ? Math.max(1, height - bw) : height
160
+
161
+ const rx = (clampedR * vbW) / effW
162
+ const ry = (clampedR * vbH) / effH
163
+
164
+ return { rx, ry }
165
+ }
166
+
167
+ const props = defineProps<{
168
+ element: any
169
+ activeSlide?: any
170
+ isSelected?: boolean
171
+ isEditingText?: boolean
172
+ selectedElementId?: string | null
173
+ themeRecipe?: any
174
+ normalizeHtmlFontFamily?: (html: string) => string
175
+ getTextStrokeStyle?: (el: any) => string
176
+ getElementTextShadow?: (el: any) => string
177
+ getReflectionLeadingCompensation?: (el: any) => number
178
+ isImagePlaceholder?: (el: any) => boolean
179
+ isReflection?: boolean
180
+ // 直接父盒子(组合)的像素尺寸,用于把 % 样式的宽高解析成 px。
181
+ // 由 CanvasGroupElement 传入;顶层元素不传,走 px 样式分支。
182
+ parentWidthPx?: number
183
+ parentHeightPx?: number
184
+ }>()
185
+
186
+ const emit = defineEmits<{
187
+ (e: 'textFocus'): void
188
+ (e: 'textBlur', ev: FocusEvent): void
189
+ (e: 'textInput', ev: Event): void
190
+ (e: 'textKeyDown', ev: KeyboardEvent): void
191
+ (e: 'startEditing', ev?: MouseEvent): void
192
+ (e: 'placeholderUpload', id: string): void
193
+ (e: 'mousemove', ev: MouseEvent): void
194
+ (e: 'mouseleave', ev: MouseEvent): void
195
+ (e: 'dblclick', ev: MouseEvent): void
196
+ }>()
197
+
198
+ // 编辑态:安装自绘选区高亮(原生 ::selection 几何跟随 CJK 字体度量,条顶空白过大)
199
+ const shapeEditableRef = ref<HTMLElement | null>(null)
200
+ const shapeTextHostRef = ref<HTMLElement | null>(null)
201
+ let selOverlay: TextSelectionOverlayHandle | null = null
202
+
203
+ const isVerticalWriting = computed(() => {
204
+ const wm = props.element.style?.writingMode
205
+ return wm === 'vertical-rl' || wm === 'vertical-lr'
206
+ })
207
+
208
+ watch([isTextSelectable, isVerticalWriting], ([selectable]) => {
209
+ nextTick(() => {
210
+ selOverlay?.destroy()
211
+ selOverlay = null
212
+ if (selectable && !isVerticalWriting.value) {
213
+ const ed = shapeEditableRef.value
214
+ const host = shapeTextHostRef.value
215
+ if (ed && host) selOverlay = createTextSelectionOverlay(ed, host)
216
+ }
217
+ })
218
+ }, { immediate: true })
219
+
220
+ onBeforeUnmount(() => {
221
+ selOverlay?.destroy()
222
+ selOverlay = null
223
+ })
224
+
225
+ function getElementWidthHeight(el: any): { width: number; height: number } {
226
+ if (!el || typeof el !== 'object') return { width: 200, height: 200 }
227
+
228
+ const wStr = el.style?.width !== undefined ? String(el.style.width).trim() : ''
229
+ const hStr = el.style?.height !== undefined ? String(el.style.height).trim() : ''
230
+
231
+ // 1. 如果有明确的 px 或非百分比数值(如 '120px', 120)
232
+ if (wStr && !wStr.endsWith('%') && wStr !== 'auto') {
233
+ const w = parseFloat(wStr)
234
+ const h = parseFloat(hStr)
235
+ if (!isNaN(w) && w > 0) {
236
+ return { width: w, height: (!isNaN(h) && h > 0) ? h : (typeof el.height === 'number' && el.height > 0 ? el.height : 200) }
237
+ }
238
+ }
239
+
240
+ // 2. 如果是 % 样式(组合内子元素):相对直接父盒子的像素尺寸解析
241
+ if (wStr.endsWith('%')) {
242
+ const pct = parseFloat(wStr)
243
+ const pW = Number(props.parentWidthPx)
244
+ const pH = Number(props.parentHeightPx)
245
+
246
+ // 优先:若 props.parentWidthPx 经 props 链传入且有效
247
+ if (!isNaN(pct) && isFinite(pW) && pW > 0) {
248
+ const width = (pct / 100) * pW
249
+ let height = NaN
250
+ const hPct = parseFloat(hStr)
251
+ if (hStr.endsWith('%') && !isNaN(hPct) && isFinite(pH) && pH > 0) {
252
+ height = (hPct / 100) * pH
253
+ } else if (hStr && hStr !== 'auto' && !hStr.endsWith('%')) {
254
+ const hn = parseFloat(hStr)
255
+ if (!isNaN(hn) && hn > 0) height = hn
256
+ }
257
+ if (isNaN(height) && typeof el.height === 'number' && el.height > 0) height = el.height
258
+ if (isNaN(height) && isFinite(pH) && pH > 0) height = pH
259
+ if (!isNaN(width) && !isNaN(height)) return { width, height: height || 200 }
260
+ }
261
+
262
+ // 关键补救:如果 parentWidthPx 尚未就绪,但 el.width / el.height 本身存在确凿数字(组合时保存的原始像素)
263
+ if (typeof el.width === 'number' && el.width > 0 && typeof el.height === 'number' && el.height > 0) {
264
+ return { width: el.width, height: el.height }
265
+ }
266
+
267
+ // 回退到画布基准 1280x720 进行百分比换算(绝不直接使用 parseFloat('38.46%') 变成 38px)
268
+ if (!isNaN(pct)) {
269
+ const width = (pct / 100) * 1280
270
+ const hPct = parseFloat(hStr)
271
+ const height = (!isNaN(hPct) && hStr.endsWith('%')) ? (hPct / 100) * 720 : (typeof el.height === 'number' && el.height > 0 ? el.height : width)
272
+ return { width, height }
273
+ }
274
+ }
275
+
276
+ // 3. 元素自身有数值型 width / height 属性
277
+ if (typeof el.width === 'number' && el.width > 0 && typeof el.height === 'number' && el.height > 0) {
278
+ return { width: el.width, height: el.height }
279
+ }
280
+
281
+ // 4. DOM 测量兜底
282
+ if (typeof window !== 'undefined' && el.id) {
283
+ const dom = document.getElementById(`canvas-el-${el.id}`)
284
+ if (dom && dom.offsetWidth > 0 && dom.offsetHeight > 0) {
285
+ return { width: dom.offsetWidth, height: dom.offsetHeight }
286
+ }
287
+ }
288
+
289
+ // 5. 最终兜底:如果是 % 严禁直接 parseFloat 当作 px
290
+ let fallbackW = 200
291
+ let fallbackH = 200
292
+ if (wStr && !wStr.endsWith('%') && !isNaN(parseFloat(wStr))) fallbackW = parseFloat(wStr)
293
+ if (hStr && !hStr.endsWith('%') && !isNaN(parseFloat(hStr))) fallbackH = parseFloat(hStr)
294
+
295
+ return { width: fallbackW, height: fallbackH }
296
+ }
297
+
298
+ function getBevelShadingPaths(el: any) {
299
+ const vW = el.viewBox ? el.viewBox[0] : 200
300
+ const vH = el.viewBox ? el.viewBox[1] : 200
301
+ const val = el.keypoints?.[0] !== undefined ? el.keypoints[0] : 0.15
302
+ const d = Math.min(vW, vH) * val
303
+ return {
304
+ left: `M 0 0 L ${d} ${d} L ${d} ${vH - d} L 0 ${vH} Z`,
305
+ top: `M 0 0 L ${vW} 0 L ${vW - d} ${d} L ${d} ${d} Z`,
306
+ bottom: `M 0 ${vH} L ${vW} ${vH} L ${vW - d} ${vH - d} L ${d} ${vH - d} Z`,
307
+ right: `M ${vW} 0 L ${vW - d} ${d} L ${vW - d} ${vH - d} L ${vW} ${vH} Z`
308
+ }
309
+ }
310
+
311
+ function getCubeShadingPaths(el: any) {
312
+ const vW = el.viewBox ? el.viewBox[0] : 200
313
+ const vH = el.viewBox ? el.viewBox[1] : 200
314
+ const val = el.keypoints?.[0] !== undefined ? el.keypoints[0] : 0.25
315
+ const d = Math.min(vW, vH) * val
316
+ return {
317
+ top: `M 0 ${d} L ${d} 0 L ${vW} 0 L ${vW - d} ${d} Z`,
318
+ right: `M ${vW - d} ${d} L ${vW} 0 L ${vW} ${vH - d} L ${vW - d} ${vH} Z`
319
+ }
320
+ }
321
+
322
+ function getRawText(el: any): string {
323
+ let val = ''
324
+ if (typeof el.text === 'string') val = el.text
325
+ else if (el.text && typeof el.text === 'object') {
326
+ val = el.text.content || el.text.text || ''
327
+ }
328
+ if (isSvgPathString(val)) return ''
329
+ return val
330
+ }
331
+
332
+ function getImagePlaceholderFill(el: any): string {
333
+ const fill = getElementFill(el)
334
+ if (!fill || fill === '#f1f5f9' || fill === '#f8fafc') {
335
+ return '#d9dde3'
336
+ }
337
+ return fill
338
+ }
339
+
340
+ function getImagePlaceholderRadius(el: any): string {
341
+ const raw = el.style?.borderRadius !== undefined
342
+ ? (typeof el.style.borderRadius === 'number' ? `${el.style.borderRadius}px` : el.style.borderRadius)
343
+ : (el.radius ? `${el.radius}px` : '0px')
344
+ if (!raw || raw === '0px' || raw === '4px') {
345
+ return '12px'
346
+ }
347
+ return raw
348
+ }
349
+
350
+ function formatShapeHtml(el: any): string {
351
+ if (props.isImagePlaceholder && props.isImagePlaceholder(el)) {
352
+ return ''
353
+ }
354
+ const raw = getRawText(el)
355
+ const norm = props.normalizeHtmlFontFamily ? props.normalizeHtmlFontFamily(raw) : raw
356
+ if (el.style?.textBackground && el.style.textBackground !== 'transparent') {
357
+ if (!norm.includes('background-color')) {
358
+ return `<span style='background-color: ${el.style.textBackground}; display: inline;'>${norm}</span>`
359
+ }
360
+ }
361
+ return norm
362
+ }
363
+ </script>
364
+
365
+ <template>
366
+ <div
367
+ class="element-content shape-element-content"
368
+ style="width: 100%; height: 100%; position: relative;"
369
+ @dblclick.stop.prevent="isReadonly ? null : (isImagePlaceholder && isImagePlaceholder(element) ? emit('placeholderUpload', element.id) : (isTextSelectable ? null : emit('startEditing', $event)))"
370
+ >
371
+ <!-- Circle Shape (with fill) -->
372
+ <div
373
+ v-if="(element.content === 'circle' || isEllipseShape(element) || element.shapType === 'ellipse' || element.style?.borderRadius === '50%') && hasElementFill(element)"
374
+ :style="{
375
+ width: '100%',
376
+ height: '100%',
377
+ backgroundColor: getElementPattern(element) ? 'transparent' : ((getElementGradientCss(element) || (getElementFill(element) !== undefined && getElementFill(element) !== 'none')) ? (getElementGradientCss(element) ? 'transparent' : getElementFill(element)) : (getElementFill(element) === 'none' ? 'transparent' : '#3b82f6')),
378
+ backgroundImage: getElementPattern(element) ? `url('${getElementPattern(element)}')` : (getElementGradientCss(element) || 'none'),
379
+ backgroundSize: getElementPattern(element) ? (isElementPatternTile(element) ? `${getElementPatternTileSize(element).w}px ${getElementPatternTileSize(element).h}px` : 'cover') : undefined,
380
+ backgroundPosition: getElementPattern(element) ? (isElementPatternTile(element) ? '0 0' : 'center center') : undefined,
381
+ backgroundRepeat: getElementPattern(element) ? (isElementPatternTile(element) ? 'repeat' : 'no-repeat') : undefined,
382
+ borderRadius: '50%',
383
+ borderWidth: (isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true)
384
+ ? '0px'
385
+ : (element.style?.enableBorder === false ? '0px' : (element.style?.borderWidth || `${getElementBorderWidth(element)}px`)),
386
+ borderStyle: element.style?.borderStyle || 'solid',
387
+ borderColor: (isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true)
388
+ ? 'transparent'
389
+ : (element.style?.enableBorder === false ? 'transparent' : (hasElementBorderGradient(element) ? 'transparent' : (element.style?.borderColor || getElementBorderColor(element) || 'transparent'))),
390
+ borderImage: hasElementBorderGradient(element) ? `${getElementBorderGradientCss(element)} 1` : undefined,
391
+ filter: (element.shadow && element.shadow.type === 'inner') ? `url(#shape-inner-shadow-${element.id})` : ''
392
+ }"
393
+ ></div>
394
+
395
+ <!-- Circle Shape (without fill) -->
396
+ <div
397
+ v-else-if="element.content === 'circle' || isEllipseShape(element) || element.shapType === 'ellipse' || element.style?.borderRadius === '50%'"
398
+ style="width: 100%; height: 100%; overflow: visible;"
399
+ >
400
+ <svg
401
+ viewBox="0 0 100 100"
402
+ preserveAspectRatio="none"
403
+ style="width: 100%; height: 100%; display: block; overflow: visible;"
404
+ :style="{ filter: (element.shadow && element.shadow.type === 'inner') ? `url(#shape-inner-shadow-${element.id})` : '' }"
405
+ >
406
+ <defs v-if="getElementBorderGradientConfig(element)">
407
+ <radialGradient
408
+ v-if="getElementBorderGradientConfig(element)!.path && getElementBorderGradientConfig(element)!.path !== 'line'"
409
+ :id="`shape-border-gradient-${svgIdKey}`"
410
+ :cx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
411
+ :cy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
412
+ :r="getElementBorderGradientConfig(element)!.path === 'rect' ? '60%' : (getElementBorderGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
413
+ :fx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
414
+ :fy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
415
+ >
416
+ <stop
417
+ v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
418
+ :key="idx"
419
+ :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
420
+ :stop-color="getHexColorWithoutAlpha(c.color)"
421
+ :stop-opacity="getAlphaFromColor(c.color)"
422
+ />
423
+ </radialGradient>
424
+ <linearGradient
425
+ v-else
426
+ :id="`shape-border-gradient-${svgIdKey}`"
427
+ :x1="getSvgGradientCoords(getElementBorderGradientConfig(element)).x1"
428
+ :y1="getSvgGradientCoords(getElementBorderGradientConfig(element)).y1"
429
+ :x2="getSvgGradientCoords(getElementBorderGradientConfig(element)).x2"
430
+ :y2="getSvgGradientCoords(getElementBorderGradientConfig(element)).y2"
431
+ >
432
+ <stop
433
+ v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
434
+ :key="idx"
435
+ :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
436
+ :stop-color="getHexColorWithoutAlpha(c.color)"
437
+ :stop-opacity="getAlphaFromColor(c.color)"
438
+ />
439
+ </linearGradient>
440
+ </defs>
441
+ <ellipse
442
+ cx="50"
443
+ cy="50"
444
+ rx="50"
445
+ ry="50"
446
+ fill="none"
447
+ :stroke="getEffectiveBorderColor(element, '#2563eb')"
448
+ :stroke-width="getEffectiveBorderWidth(element)"
449
+ :stroke-dasharray="element.style?.borderStyle === 'dashed' || element.borderType === 'dashed' ? '5 5' : (element.style?.borderStyle === 'dotted' ? '2 2' : '')"
450
+ vector-effect="non-scaling-stroke"
451
+ />
452
+ </svg>
453
+ </div>
454
+
455
+ <!-- Triangle Shape -->
456
+ <div v-else-if="element.content === 'triangle'" style="width: 100%; height: 100%; overflow: visible;">
457
+ <svg
458
+ viewBox="0 0 100 100"
459
+ preserveAspectRatio="none"
460
+ style="width: 100%; height: 100%; display: block; overflow: visible;"
461
+ :style="{ filter: (element.shadow && element.shadow.type === 'inner') ? `url(#shape-inner-shadow-${element.id})` : '' }"
462
+ >
463
+ <defs>
464
+ <pattern
465
+ v-if="getElementPattern(element)"
466
+ :id="`shape-pattern-${svgIdKey}`"
467
+ :width="isElementPatternTile(element) ? getElementPatternTileSize(element).w : '100%'"
468
+ :height="isElementPatternTile(element) ? getElementPatternTileSize(element).h : '100%'"
469
+ :patternUnits="isElementPatternTile(element) ? 'userSpaceOnUse' : 'objectBoundingBox'"
470
+ :patternContentUnits="isElementPatternTile(element) ? 'userSpaceOnUse' : 'objectBoundingBox'"
471
+ :patternTransform="getShapePatternTransform(element, true)"
472
+ >
473
+ <image
474
+ :href="getElementPattern(element)"
475
+ :width="isElementPatternTile(element) ? getElementPatternTileSize(element).w : 1"
476
+ :height="isElementPatternTile(element) ? getElementPatternTileSize(element).h : 1"
477
+ preserveAspectRatio="none"
478
+ />
479
+ </pattern>
480
+ <template v-if="getElementGradientConfig(element)">
481
+ <radialGradient
482
+ v-if="getElementGradientConfig(element)!.path && getElementGradientConfig(element)!.path !== 'line'"
483
+ :id="`shape-gradient-${svgIdKey}`"
484
+ :cx="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.x * 100) : 50}%`"
485
+ :cy="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.y * 100) : 50}%`"
486
+ :r="getElementGradientConfig(element)!.path === 'rect' ? '60%' : (getElementGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
487
+ :fx="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.x * 100) : 50}%`"
488
+ :fy="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.y * 100) : 50}%`"
489
+ >
490
+ <stop
491
+ v-for="(c, idx) in (getElementGradientConfig(element)?.colors || [])"
492
+ :key="idx"
493
+ :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
494
+ :stop-color="getHexColorWithoutAlpha(c.color)"
495
+ :stop-opacity="getAlphaFromColor(c.color)"
496
+ />
497
+ </radialGradient>
498
+ <linearGradient
499
+ v-else
500
+ :id="`shape-gradient-${svgIdKey}`"
501
+ :x1="getSvgGradientCoords(element).x1"
502
+ :y1="getSvgGradientCoords(element).y1"
503
+ :x2="getSvgGradientCoords(element).x2"
504
+ :y2="getSvgGradientCoords(element).y2"
505
+ >
506
+ <stop
507
+ v-for="(c, idx) in (getElementGradientConfig(element)?.colors || [])"
508
+ :key="idx"
509
+ :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
510
+ :stop-color="getHexColorWithoutAlpha(c.color)"
511
+ :stop-opacity="getAlphaFromColor(c.color)"
512
+ />
513
+ </linearGradient>
514
+ </template>
515
+ <template v-if="getElementBorderGradientConfig(element)">
516
+ <radialGradient
517
+ v-if="getElementBorderGradientConfig(element)!.path && getElementBorderGradientConfig(element)!.path !== 'line'"
518
+ :id="`shape-border-gradient-${svgIdKey}`"
519
+ :cx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
520
+ :cy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
521
+ :r="getElementBorderGradientConfig(element)!.path === 'rect' ? '60%' : (getElementBorderGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
522
+ :fx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
523
+ :fy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
524
+ >
525
+ <stop
526
+ v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
527
+ :key="idx"
528
+ :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
529
+ :stop-color="getHexColorWithoutAlpha(c.color)"
530
+ :stop-opacity="getAlphaFromColor(c.color)"
531
+ />
532
+ </radialGradient>
533
+ <linearGradient
534
+ v-else
535
+ :id="`shape-border-gradient-${svgIdKey}`"
536
+ :x1="getSvgGradientCoords(getElementBorderGradientConfig(element)).x1"
537
+ :y1="getSvgGradientCoords(getElementBorderGradientConfig(element)).y1"
538
+ :x2="getSvgGradientCoords(getElementBorderGradientConfig(element)).x2"
539
+ :y2="getSvgGradientCoords(getElementBorderGradientConfig(element)).y2"
540
+ >
541
+ <stop
542
+ v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
543
+ :key="idx"
544
+ :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
545
+ :stop-color="getHexColorWithoutAlpha(c.color)"
546
+ :stop-opacity="getAlphaFromColor(c.color)"
547
+ />
548
+ </linearGradient>
549
+ </template>
550
+ </defs>
551
+ <polygon
552
+ points="50,0 0,100 100,100"
553
+ :fill="hasElementFill(element) ? (getElementPattern(element) ? `url(#shape-pattern-${svgIdKey})` : (getElementGradientConfig(element) ? `url(#shape-gradient-${svgIdKey})` : (element.style?.fill !== undefined ? element.style.fill : '#f59e0b'))) : 'none'"
554
+ :stroke="getEffectiveBorderColor(element, '#d97706')"
555
+ :stroke-width="getEffectiveBorderWidth(element)"
556
+ :stroke-dasharray="element.style?.borderStyle === 'dashed' ? '5 5' : ''"
557
+ />
558
+ </svg>
559
+ </div>
560
+
561
+ <!-- SVG Path Shapes -->
562
+ <div
563
+ v-else-if="(element.path || element.content) && ((element.path || element.content).trim().startsWith('M') || (element.path || element.content).trim().startsWith('m'))"
564
+ style="width: 100%; height: 100%; overflow: visible;"
565
+ >
566
+ <svg
567
+ overflow="visible"
568
+ style="width: 100%; height: 100%; display: block; overflow: visible;"
569
+ :style="{ filter: (element.shadow && element.shadow.type === 'inner') ? `url(#shape-inner-shadow-${element.id})` : '' }"
570
+ >
571
+ <defs>
572
+ <pattern
573
+ v-if="getElementPattern(element)"
574
+ :id="`shape-pattern-${svgIdKey}`"
575
+ :width="isElementPatternTile(element) ? getElementPatternTileSize(element).w : '100%'"
576
+ :height="isElementPatternTile(element) ? getElementPatternTileSize(element).h : '100%'"
577
+ :patternUnits="isElementPatternTile(element) ? 'userSpaceOnUse' : 'objectBoundingBox'"
578
+ :patternContentUnits="isElementPatternTile(element) ? 'userSpaceOnUse' : 'objectBoundingBox'"
579
+ :patternTransform="getShapePatternTransform(element, false)"
580
+ >
581
+ <image
582
+ :href="getElementPattern(element)"
583
+ :width="isElementPatternTile(element) ? getElementPatternTileSize(element).w : 1"
584
+ :height="isElementPatternTile(element) ? getElementPatternTileSize(element).h : 1"
585
+ preserveAspectRatio="none"
586
+ />
587
+ </pattern>
588
+ <template v-if="getElementGradientConfig(element)">
589
+ <radialGradient
590
+ v-if="getElementGradientConfig(element)!.path && getElementGradientConfig(element)!.path !== 'line'"
591
+ :id="`shape-gradient-${svgIdKey}`"
592
+ :cx="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.x * 100) : 50}%`"
593
+ :cy="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.y * 100) : 50}%`"
594
+ :r="getElementGradientConfig(element)!.path === 'rect' ? '60%' : (getElementGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
595
+ :fx="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.x * 100) : 50}%`"
596
+ :fy="`${getElementGradientConfig(element)!.center ? Math.round(getElementGradientConfig(element)!.center!.y * 100) : 50}%`"
597
+ >
598
+ <stop
599
+ v-for="(c, idx) in (getElementGradientConfig(element)?.colors || [])"
600
+ :key="idx"
601
+ :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
602
+ :stop-color="getHexColorWithoutAlpha(c.color)"
603
+ :stop-opacity="getAlphaFromColor(c.color)"
604
+ />
605
+ </radialGradient>
606
+ <linearGradient
607
+ v-else
608
+ :id="`shape-gradient-${svgIdKey}`"
609
+ :x1="getSvgGradientCoords(element).x1"
610
+ :y1="getSvgGradientCoords(element).y1"
611
+ :x2="getSvgGradientCoords(element).x2"
612
+ :y2="getSvgGradientCoords(element).y2"
613
+ >
614
+ <stop
615
+ v-for="(c, idx) in (getElementGradientConfig(element)?.colors || [])"
616
+ :key="idx"
617
+ :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
618
+ :stop-color="getHexColorWithoutAlpha(c.color)"
619
+ :stop-opacity="getAlphaFromColor(c.color)"
620
+ />
621
+ </linearGradient>
622
+ </template>
623
+ <template v-if="getElementBorderGradientConfig(element)">
624
+ <radialGradient
625
+ v-if="getElementBorderGradientConfig(element)!.path && getElementBorderGradientConfig(element)!.path !== 'line'"
626
+ :id="`shape-border-gradient-${svgIdKey}`"
627
+ :cx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
628
+ :cy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
629
+ :r="getElementBorderGradientConfig(element)!.path === 'rect' ? '60%' : (getElementBorderGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
630
+ :fx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
631
+ :fy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
632
+ >
633
+ <stop
634
+ v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
635
+ :key="idx"
636
+ :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
637
+ :stop-color="getHexColorWithoutAlpha(c.color)"
638
+ :stop-opacity="getAlphaFromColor(c.color)"
639
+ />
640
+ </radialGradient>
641
+ <linearGradient
642
+ v-else
643
+ :id="`shape-border-gradient-${svgIdKey}`"
644
+ :x1="getSvgGradientCoords(getElementBorderGradientConfig(element)).x1"
645
+ :y1="getSvgGradientCoords(getElementBorderGradientConfig(element)).y1"
646
+ :x2="getSvgGradientCoords(getElementBorderGradientConfig(element)).x2"
647
+ :y2="getSvgGradientCoords(getElementBorderGradientConfig(element)).y2"
648
+ >
649
+ <stop
650
+ v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
651
+ :key="idx"
652
+ :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
653
+ :stop-color="getHexColorWithoutAlpha(c.color)"
654
+ :stop-opacity="getAlphaFromColor(c.color)"
655
+ />
656
+ </linearGradient>
657
+ </template>
658
+ </defs>
659
+ <g
660
+ :transform="getShapeSvgTransform(element)"
661
+ >
662
+ <rect
663
+ v-if="isRectShape(element)"
664
+ x="0"
665
+ y="0"
666
+ :width="getShapeVb(element).w"
667
+ :height="getShapeVb(element).h"
668
+ :rx="getRectRadiusInVb(element).rx"
669
+ :ry="getRectRadiusInVb(element).ry"
670
+ vector-effect="non-scaling-stroke"
671
+ :fill="hasElementFill(element) ? (getElementPattern(element) ? `url(#shape-pattern-${svgIdKey})` : (getElementGradientConfig(element) ? `url(#shape-gradient-${svgIdKey})` : (getElementFill(element) !== undefined ? getElementFill(element) : '#3b82f6'))) : 'none'"
672
+ :stroke="(isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true) ? 'none' : (element.style?.enableBorder === false ? 'none' : getEffectiveBorderColor(element, 'none'))"
673
+ :stroke-width="(isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true) ? 0 : (element.style?.enableBorder === false ? 0 : getElementBorderWidth(element))"
674
+ :stroke-linecap="element.style?.strokeLinecap || element.borderLineCap || element.lineCap || (element.shapType === 'arc' ? 'round' : (isOpenLineShape(element) ? 'butt' : undefined))"
675
+ :stroke-dasharray="element.style?.borderStyle === 'dashed' || element.borderType === 'dashed' ? '5 5' : ''"
676
+ />
677
+ <path
678
+ v-else
679
+ vector-effect="non-scaling-stroke"
680
+ :d="element.path || element.content"
681
+ :fill="hasElementFill(element) ? (getElementPattern(element) ? `url(#shape-pattern-${svgIdKey})` : (getElementGradientConfig(element) ? `url(#shape-gradient-${svgIdKey})` : (getElementFill(element) !== undefined ? getElementFill(element) : '#3b82f6'))) : 'none'"
682
+ :stroke="(isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true) ? 'none' : (element.style?.enableBorder === false ? 'none' : getEffectiveBorderColor(element, 'none'))"
683
+ :stroke-width="(isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true) ? 0 : (element.style?.enableBorder === false ? 0 : getElementBorderWidth(element))"
684
+ :stroke-linecap="element.style?.strokeLinecap || element.borderLineCap || element.lineCap || (element.shapType === 'arc' ? 'round' : (isOpenLineShape(element) ? 'butt' : undefined))"
685
+ :stroke-dasharray="element.style?.borderStyle === 'dashed' || element.borderType === 'dashed' ? '5 5' : ''"
686
+ />
687
+ <template v-if="element.pathFormula === 'bevel'">
688
+ <path :d="getBevelShadingPaths(element).left" fill="white" fill-opacity="0.2" stroke="none" pointer-events="none" />
689
+ <path :d="getBevelShadingPaths(element).top" fill="black" fill-opacity="0.08" stroke="none" pointer-events="none" />
690
+ <path :d="getBevelShadingPaths(element).bottom" fill="black" fill-opacity="0.15" stroke="none" pointer-events="none" />
691
+ <path :d="getBevelShadingPaths(element).right" fill="black" fill-opacity="0.25" stroke="none" pointer-events="none" />
692
+ </template>
693
+ <template v-if="element.pathFormula === 'cube'">
694
+ <path :d="getCubeShadingPaths(element).top" fill="white" fill-opacity="0.15" stroke="none" pointer-events="none" />
695
+ <path :d="getCubeShadingPaths(element).right" fill="black" fill-opacity="0.2" stroke="none" pointer-events="none" />
696
+ </template>
697
+ </g>
698
+ </svg>
699
+ </div>
700
+
701
+ <!-- Default Rectangle Shape (with fill) -->
702
+ <div
703
+ v-else-if="hasElementFill(element)"
704
+ :style="{
705
+ width: '100%',
706
+ height: '100%',
707
+ backgroundColor: getElementPattern(element) ? 'transparent' : ((isImagePlaceholder && isImagePlaceholder(element) && !getElementGradientCss(element)) ? getImagePlaceholderFill(element) : ((getElementGradientCss(element) || (getElementFill(element) !== undefined && getElementFill(element) !== 'none')) ? (getElementGradientCss(element) ? 'transparent' : getElementFill(element)) : (getElementFill(element) === 'none' ? 'transparent' : '#10b981'))),
708
+ backgroundImage: getElementPattern(element) ? `url('${getElementPattern(element)}')` : (getElementGradientCss(element) || 'none'),
709
+ backgroundSize: getElementPattern(element) ? (isElementPatternTile(element) ? `${getElementPatternTileSize(element).w}px ${getElementPatternTileSize(element).h}px` : 'cover') : undefined,
710
+ backgroundPosition: getElementPattern(element) ? (isElementPatternTile(element) ? '0 0' : 'center center') : undefined,
711
+ backgroundRepeat: getElementPattern(element) ? (isElementPatternTile(element) ? 'repeat' : 'no-repeat') : undefined,
712
+ borderRadius: (isImagePlaceholder && isImagePlaceholder(element)) ? getImagePlaceholderRadius(element) : (element.style?.borderRadius !== undefined ? (typeof element.style.borderRadius === 'number' ? `${element.style.borderRadius}px` : element.style.borderRadius) : (element.radius ? `${element.radius}px` : '0px')),
713
+ borderWidth: (isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true)
714
+ ? '0px'
715
+ : (element.style?.enableBorder === false ? '0px' : `${getElementBorderWidth(element)}px`),
716
+ borderStyle: element.style?.borderStyle || element.borderType || 'solid',
717
+ borderColor: (isImagePlaceholder && isImagePlaceholder(element) && element.style?.enableBorder !== true)
718
+ ? 'transparent'
719
+ : (element.style?.enableBorder === false ? 'transparent' : (hasElementBorderGradient(element) ? 'transparent' : (element.style?.borderColor || getElementBorderColor(element) || 'transparent'))),
720
+ borderImage: hasElementBorderGradient(element) ? `${getElementBorderGradientCss(element)} 1` : undefined,
721
+ filter: (element.shadow && element.shadow.type === 'inner') ? `url(#shape-inner-shadow-${element.id})` : ''
722
+ }"
723
+ ></div>
724
+
725
+ <!-- Default Rectangle Shape (without fill) -->
726
+ <div
727
+ v-else
728
+ style="width: 100%; height: 100%; overflow: visible;"
729
+ >
730
+ <svg
731
+ style="width: 100%; height: 100%; display: block; overflow: visible;"
732
+ :style="{ filter: (element.shadow && element.shadow.type === 'inner') ? `url(#shape-inner-shadow-${element.id})` : '' }"
733
+ >
734
+ <defs v-if="getElementBorderGradientConfig(element)">
735
+ <radialGradient
736
+ v-if="getElementBorderGradientConfig(element)!.path && getElementBorderGradientConfig(element)!.path !== 'line'"
737
+ :id="`shape-border-gradient-${svgIdKey}`"
738
+ :cx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
739
+ :cy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
740
+ :r="getElementBorderGradientConfig(element)!.path === 'rect' ? '60%' : (getElementBorderGradientConfig(element)!.path === 'path' ? '70%' : '50%')"
741
+ :fx="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.x * 100) : 50}%`"
742
+ :fy="`${getElementBorderGradientConfig(element)!.center ? Math.round(getElementBorderGradientConfig(element)!.center!.y * 100) : 50}%`"
743
+ >
744
+ <stop
745
+ v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
746
+ :key="idx"
747
+ :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
748
+ :stop-color="getHexColorWithoutAlpha(c.color)"
749
+ :stop-opacity="getAlphaFromColor(c.color)"
750
+ />
751
+ </radialGradient>
752
+ <linearGradient
753
+ v-else
754
+ :id="`shape-border-gradient-${svgIdKey}`"
755
+ :x1="getSvgGradientCoords(getElementBorderGradientConfig(element)).x1"
756
+ :y1="getSvgGradientCoords(getElementBorderGradientConfig(element)).y1"
757
+ :x2="getSvgGradientCoords(getElementBorderGradientConfig(element)).x2"
758
+ :y2="getSvgGradientCoords(getElementBorderGradientConfig(element)).y2"
759
+ >
760
+ <stop
761
+ v-for="(c, idx) in (getElementBorderGradientConfig(element)?.colors || [])"
762
+ :key="idx"
763
+ :offset="typeof c.pos === 'string' ? (c.pos.endsWith('%') ? c.pos : c.pos + '%') : `${c.pos}%`"
764
+ :stop-color="getHexColorWithoutAlpha(c.color)"
765
+ :stop-opacity="getAlphaFromColor(c.color)"
766
+ />
767
+ </linearGradient>
768
+ </defs>
769
+ <rect
770
+ x="0"
771
+ y="0"
772
+ width="100%"
773
+ height="100%"
774
+ :rx="parseFloat(element.style?.borderRadius) || parseFloat(element.radius) || 0"
775
+ :ry="parseFloat(element.style?.borderRadius) || parseFloat(element.radius) || 0"
776
+ fill="none"
777
+ :stroke="getEffectiveBorderColor(element, '#10b981')"
778
+ :stroke-width="getEffectiveBorderWidth(element)"
779
+ :stroke-dasharray="element.style?.borderStyle === 'dashed' || element.borderType === 'dashed' ? '5 5' : (element.style?.borderStyle === 'dotted' ? '2 2' : '')"
780
+ vector-effect="non-scaling-stroke"
781
+ />
782
+ </svg>
783
+ </div>
784
+
785
+ <!-- Image Placeholder Centered Icon Overlay -->
786
+ <div
787
+ v-if="isImagePlaceholder && isImagePlaceholder(element)"
788
+ class="image-placeholder-icon-overlay"
789
+ style="position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; pointer-events: none;"
790
+ >
791
+ <svg
792
+ class="image-placeholder-icon-btn"
793
+ width="56"
794
+ height="56"
795
+ viewBox="0 0 24 24"
796
+ fill="none"
797
+ style="max-width: 45%; max-height: 45%; cursor: pointer; transition: opacity 0.15s ease, transform 0.15s ease;"
798
+ :style="{ pointerEvents: isReflection ? 'none' : 'auto' }"
799
+ >
800
+ <rect x="3" y="3" width="18" height="18" rx="2" stroke="#475569" stroke-width="2" />
801
+ <circle cx="8.5" cy="8.5" r="1.5" fill="#475569" />
802
+ <path d="M21 15l-5-5L5 21" stroke="#475569" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
803
+ </svg>
804
+ </div>
805
+
806
+ <!-- Embedded Shape Text Overlay -->
807
+ <div
808
+ v-else
809
+ class="shape-text-overlay-wrapper"
810
+ style="position: absolute; display: flex; flex-direction: column; box-sizing: border-box; pointer-events: none;"
811
+ :style="{
812
+ left: isEllipseShape(element) ? '14.6%' : '0px',
813
+ top: isEllipseShape(element) ? '14.6%' : '0px',
814
+ width: isEllipseShape(element) ? '70.8%' : '100%',
815
+ height: isEllipseShape(element) ? '70.8%' : '100%',
816
+ justifyContent: (element.style?.verticalAlign === 'top' || element.style?.verticalAlign === 'up') ? 'flex-start' : ((element.style?.verticalAlign === 'bottom' || element.style?.verticalAlign === 'down') ? 'flex-end' : 'center'),
817
+ padding: isEllipseShape(element) ? '0px' : (element.style?.padding !== undefined ? element.style.padding : '4.8px 9.6px 4.8px 9.6px'),
818
+ transform: ((element.style as any)?.flipH || element.flipH || element.isFlipH || (element.style as any)?.flipV || element.flipV || element.isFlipV) ? `scale(${((element.style as any)?.flipH || element.flipH || element.isFlipH) ? -1 : 1}, ${((element.style as any)?.flipV || element.flipV || element.isFlipV) ? -1 : 1})` : undefined
819
+ }"
820
+ >
821
+ <div ref="shapeTextHostRef" class="text-reflection-wrapper" style="position: relative; pointer-events: none;" :style="{
822
+ width: (element.style?.writingMode === 'vertical-rl' || element.style?.writingMode === 'vertical-lr') ? 'auto' : '100%',
823
+ height: (element.style?.writingMode === 'vertical-rl' || element.style?.writingMode === 'vertical-lr') ? '100%' : 'auto'
824
+ }">
825
+ <div
826
+ ref="shapeEditableRef"
827
+ :id="isReflection ? undefined : `editable-${element.id}`"
828
+ :contenteditable="isTextSelectable ? 'true' : 'false'"
829
+ @mousedown="isTextSelectable ? $event.stopPropagation() : null"
830
+ @click="isTextSelectable ? $event.stopPropagation() : null"
831
+ @focusin="emit('textFocus')"
832
+ @focusout="emit('textBlur', $event)"
833
+ @input="emit('textInput', $event)"
834
+ @keydown="emit('textKeyDown', $event)"
835
+ @mousemove="emit('mousemove', $event)"
836
+ @mouseleave="emit('mouseleave', $event)"
837
+ @dblclick.stop.prevent="isReadonly ? null : (isImagePlaceholder && isImagePlaceholder(element) ? emit('placeholderUpload', element.id) : (isTextSelectable ? null : emit('startEditing', $event)))"
838
+ class="shape-text-overlay"
839
+ :style="{
840
+ display: 'block',
841
+ writingMode: element.style?.writingMode || 'horizontal-tb',
842
+ width: (element.style?.writingMode === 'vertical-rl' || element.style?.writingMode === 'vertical-lr') ? 'auto' : '100%',
843
+ height: (element.style?.writingMode === 'vertical-rl' || element.style?.writingMode === 'vertical-lr') ? '100%' : 'auto',
844
+ zoom: (element.autoFit?.type === 'text' && element.autoFit?.fontScale) ? (element.autoFit.fontScale / 100) : 1,
845
+ transform: 'none',
846
+ maxWidth: '100%',
847
+ outline: 'none',
848
+ padding: '0px',
849
+ boxSizing: 'content-box',
850
+ overflow: 'wrap',
851
+ wordBreak: 'break-word',
852
+ pointerEvents: isReflection ? 'none' : (isTextSelectable ? 'auto' : (getRawText(element).trim() ? 'auto' : 'none')),
853
+ cursor: isTextSelectable ? 'text' : undefined,
854
+ userSelect: isTextSelectable ? 'text' : 'none',
855
+ color: element.style?.textColor || element.style?.color || element.textColor || ((isImagePlaceholder && isImagePlaceholder(element)) ? '#6366f1' : (element.style?.fill === '#ffffff' || element.style?.fill === '#FFFFFF' ? '#475569' : (themeRecipe?.shape?.textColor || '#ffffff'))),
856
+ fontFamily: (element.style?.fontFamily && element.style?.fontFamily !== '默认字体' && element.style?.fontFamily !== 'inherit')
857
+ ? element.style.fontFamily
858
+ : ((element.fontFamily && element.fontFamily !== '默认字体' && element.fontFamily !== 'inherit')
859
+ ? element.fontFamily
860
+ : (themeRecipe?.styleFonts?.bodyFont || 'SourceHanSansCN')),
861
+ fontSize: element.style?.fontSize || ((isImagePlaceholder && isImagePlaceholder(element)) ? '15px' : '16px'),
862
+ fontWeight: element.style?.fontWeight || ((isImagePlaceholder && isImagePlaceholder(element)) ? '600' : (getFontVariantStyle(element.style?.fontFamily || element.fontFamily, element.style?.fontFamilyStyle).fontWeight !== 'normal' ? getFontVariantStyle(element.style?.fontFamily || element.fontFamily, element.style?.fontFamilyStyle).fontWeight : 'normal')),
863
+ fontStyle: element.style?.fontStyle || getFontVariantStyle(element.style?.fontFamily || element.fontFamily, element.style?.fontFamilyStyle).fontStyle,
864
+ textDecoration: element.style?.textDecoration || 'none',
865
+ lineHeight: element.style?.lineHeight || 1.35,
866
+ textAlign: (element.style?.textAlign === 'distribute' ? 'justify' : (element.style?.textAlign || 'center')) as any,
867
+ textAlignLast: (element.style?.textAlign === 'distribute' ? 'justify' : 'auto') as any,
868
+ textJustify: (element.style?.textAlign === 'distribute' ? 'inter-character' : 'auto') as any,
869
+ whiteSpace: element.style?.whiteSpace || 'normal',
870
+ ['-webkit-text-stroke']: getTextStrokeStyle ? getTextStrokeStyle(element) : 'none',
871
+ ['paint-order']: element.style?.enableTextStroke ? 'stroke fill' : 'normal',
872
+ ['text-shadow']: getElementTextShadow ? getElementTextShadow(element) : 'none',
873
+ '--paragraphSpace': `${element.style?.paragraphSpace === undefined ? 3 : element.style.paragraphSpace}px`
874
+ }"
875
+ v-html="formatShapeHtml(element)"
876
+ ></div>
877
+ </div>
878
+ </div>
879
+ </div>
880
+ </template>
881
+
882
+ <style scoped>
883
+ .shape-element-content {
884
+ user-select: none;
885
+ }
886
+ .shape-text-overlay {
887
+ user-select: text;
888
+ -webkit-user-select: text;
889
+ }
890
+ .image-placeholder-icon-btn:hover {
891
+ opacity: 0.78;
892
+ transform: scale(1.06);
893
+ }
894
+ </style>