mirror of
https://github.com/grafana/grafana.git
synced 2025-12-21 03:54:29 +08:00
Compare commits
2 Commits
zoltan/pos
...
drew08t/ca
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8518c238ea | ||
|
|
0317774984 |
@@ -79,6 +79,7 @@ The following sections describe the different elements available.
|
||||
- [Triangle](#basic-shapes)
|
||||
- [Cloud](#basic-shapes)
|
||||
- [Parallelogram](#basic-shapes)
|
||||
- [SVG](#svg)
|
||||
- [Button](#button)
|
||||
|
||||
{{< /column-list >}}
|
||||
@@ -127,6 +128,20 @@ The server element lets you easily represent a single server, a stack of servers
|
||||
|
||||
{{< figure src="/media/docs/grafana/canvas-server-element-9-4-0.png" max-width="650px" alt="Canvas server element" >}}
|
||||
|
||||
#### SVG
|
||||
|
||||
The SVG element lets you add custom SVG graphics to the canvas. You can enter raw SVG markup in the content field, and the element will render it with proper sanitization to prevent XSS attacks. This element is useful for creating custom icons, logos, or complex graphics that aren't available in the standard shape elements.
|
||||
|
||||
SVG element features:
|
||||
|
||||
- **Sanitized content**: All SVG content is automatically sanitized for security
|
||||
- **Data binding**: SVG content can be bound to field data using template variables
|
||||
- **Scalable**: SVG graphics scale cleanly at any size
|
||||
|
||||
The SVG element supports the following configuration options:
|
||||
|
||||
- **SVG Content**: Enter raw SVG markup. Content will be sanitized automatically.
|
||||
|
||||
#### Button
|
||||
|
||||
The button element lets you add a basic button to the canvas. Button elements support triggering basic, unauthenticated API calls. [API settings](#button-api-options) are found in the button element editor. You can also pass template variables in the API editor.
|
||||
|
||||
161
public/app/features/canvas/elements/svg.tsx
Normal file
161
public/app/features/canvas/elements/svg.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { css } from '@emotion/css';
|
||||
|
||||
import { GrafanaTheme2, textUtil } from '@grafana/data';
|
||||
import { t } from '@grafana/i18n';
|
||||
import { TextDimensionConfig, TextDimensionMode } from '@grafana/schema';
|
||||
import { CodeEditor, useStyles2 } from '@grafana/ui';
|
||||
import { DimensionContext } from 'app/features/dimensions/context';
|
||||
|
||||
import { CanvasElementItem, CanvasElementOptions, CanvasElementProps } from '../element';
|
||||
|
||||
export interface SvgConfig {
|
||||
content?: TextDimensionConfig;
|
||||
}
|
||||
|
||||
interface SvgData {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export function SvgDisplay(props: CanvasElementProps<SvgConfig, SvgData>) {
|
||||
const { data } = props;
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
if (!data?.content) {
|
||||
return (
|
||||
<div className={styles.placeholder}>{t('canvas.svg-element.placeholder', 'Double click to add SVG content')}</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Check if content already has an SVG wrapper
|
||||
const hasSvgWrapper = data.content.trim().toLowerCase().startsWith('<svg');
|
||||
|
||||
let sanitizedContent: string;
|
||||
|
||||
if (hasSvgWrapper) {
|
||||
// Content already has SVG wrapper, sanitize as-is
|
||||
sanitizedContent = textUtil.sanitizeSVGContent(data.content);
|
||||
} else {
|
||||
// Content is a fragment - wrap in SVG before sanitizing
|
||||
const wrappedContent = `<svg width="100%" height="100%">${data.content}</svg>`;
|
||||
sanitizedContent = textUtil.sanitizeSVGContent(wrappedContent);
|
||||
}
|
||||
|
||||
return <div className={styles.container} dangerouslySetInnerHTML={{ __html: sanitizedContent }} />;
|
||||
}
|
||||
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
container: css({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
'& svg': {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
},
|
||||
}),
|
||||
placeholder: css({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: theme.colors.text.secondary,
|
||||
fontSize: theme.typography.bodySmall.fontSize,
|
||||
textAlign: 'center',
|
||||
padding: theme.spacing(1),
|
||||
border: `1px dashed ${theme.colors.border.weak}`,
|
||||
borderRadius: theme.shape.radius.default,
|
||||
}),
|
||||
});
|
||||
|
||||
export const svgItem: CanvasElementItem<SvgConfig, SvgData> = {
|
||||
id: 'svg',
|
||||
name: 'SVG',
|
||||
description: 'Generic SVG element with sanitized content',
|
||||
|
||||
display: SvgDisplay,
|
||||
|
||||
hasEditMode: false,
|
||||
|
||||
defaultSize: {
|
||||
width: 100,
|
||||
height: 100,
|
||||
},
|
||||
|
||||
getNewOptions: (options) => ({
|
||||
...options,
|
||||
config: {
|
||||
content: {
|
||||
mode: TextDimensionMode.Fixed,
|
||||
fixed: '<svg viewBox="0 0 100 100"><circle cx="50" cy="50" r="40" fill="currentColor" /></svg>',
|
||||
},
|
||||
},
|
||||
background: {
|
||||
color: {
|
||||
fixed: 'transparent',
|
||||
},
|
||||
},
|
||||
placement: {
|
||||
width: options?.placement?.width ?? 100,
|
||||
height: options?.placement?.height ?? 100,
|
||||
top: options?.placement?.top ?? 100,
|
||||
left: options?.placement?.left ?? 100,
|
||||
rotation: options?.placement?.rotation ?? 0,
|
||||
},
|
||||
links: options?.links ?? [],
|
||||
}),
|
||||
|
||||
prepareData: (dimensionContext: DimensionContext, elementOptions: CanvasElementOptions<SvgConfig>) => {
|
||||
const svgConfig = elementOptions.config;
|
||||
|
||||
const data: SvgData = {
|
||||
content: svgConfig?.content ? dimensionContext.getText(svgConfig.content).value() : '',
|
||||
};
|
||||
|
||||
return data;
|
||||
},
|
||||
|
||||
registerOptionsUI: (builder) => {
|
||||
const category = [t('canvas.svg-element.category', 'SVG')];
|
||||
|
||||
builder.addCustomEditor({
|
||||
category,
|
||||
id: 'svgContent',
|
||||
path: 'config.content',
|
||||
name: t('canvas.svg-element.content', 'SVG Content'),
|
||||
description: t('canvas.svg-element.content-description', 'Enter SVG content.'),
|
||||
editor: ({ value, onChange }) => {
|
||||
const currentValue = value?.fixed || '';
|
||||
|
||||
return (
|
||||
<CodeEditor
|
||||
value={currentValue}
|
||||
language="xml"
|
||||
height="200px"
|
||||
onBlur={(newValue) => {
|
||||
onChange({
|
||||
...value,
|
||||
mode: TextDimensionMode.Fixed,
|
||||
fixed: newValue,
|
||||
});
|
||||
}}
|
||||
monacoOptions={{
|
||||
minimap: { enabled: false },
|
||||
lineNumbers: 'on',
|
||||
wordWrap: 'on',
|
||||
scrollBeyondLastLine: false,
|
||||
folding: false,
|
||||
renderLineHighlight: 'none',
|
||||
overviewRulerBorder: false,
|
||||
hideCursorInOverviewRuler: true,
|
||||
overviewRulerLanes: 0,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
settings: {},
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -12,6 +12,7 @@ import { metricValueItem } from './elements/metricValue';
|
||||
import { parallelogramItem } from './elements/parallelogram';
|
||||
import { rectangleItem } from './elements/rectangle';
|
||||
import { serverItem } from './elements/server/server';
|
||||
import { svgItem } from './elements/svg';
|
||||
import { textItem } from './elements/text';
|
||||
import { triangleItem } from './elements/triangle';
|
||||
import { windTurbineItem } from './elements/windTurbine';
|
||||
@@ -33,6 +34,7 @@ export const defaultElementItems = [
|
||||
triangleItem,
|
||||
cloudItem,
|
||||
parallelogramItem,
|
||||
svgItem,
|
||||
];
|
||||
|
||||
export const advancedElementItems = [buttonItem, windTurbineItem, droneTopItem, droneFrontItem, droneSideItem];
|
||||
|
||||
@@ -6,7 +6,7 @@ import { Key, useEffect, useMemo, useState } from 'react';
|
||||
import { GrafanaTheme2, StandardEditorProps } from '@grafana/data';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { config } from '@grafana/runtime';
|
||||
import { Button, Icon, Stack, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { Button, FileUpload, Icon, Stack, useStyles2, useTheme2 } from '@grafana/ui';
|
||||
import { AddLayerButton } from 'app/core/components/Layers/AddLayerButton';
|
||||
import { ElementState } from 'app/features/canvas/runtime/element';
|
||||
import { frameSelection, reorderElements } from 'app/features/canvas/runtime/sceneElementManagement';
|
||||
@@ -14,7 +14,7 @@ import { frameSelection, reorderElements } from 'app/features/canvas/runtime/sce
|
||||
import { getGlobalStyles } from '../../globalStyles';
|
||||
import { Options } from '../../panelcfg.gen';
|
||||
import { DragNode, DropNode } from '../../types';
|
||||
import { doSelect, getElementTypes, onAddItem } from '../../utils';
|
||||
import { doSelect, getElementTypes, onAddItem, onImportFile } from '../../utils';
|
||||
import { TreeViewEditorProps } from '../element/elementEditor';
|
||||
|
||||
import { TreeNodeTitle } from './TreeNodeTitle';
|
||||
@@ -165,6 +165,16 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<unknown, Tree
|
||||
label={t('canvas.tree-navigation-editor.label-add-item', 'Add item')}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.uploadFileButtonDiv}>
|
||||
<FileUpload
|
||||
className={styles.uploadFileButton}
|
||||
size="sm"
|
||||
accept=".svg"
|
||||
onFileUpload={({ currentTarget }) => onImportFile(currentTarget, layer)}
|
||||
>
|
||||
<span>{t('canvas.tree-navigation-editor.upload-SVG-file', 'Upload SVG file')}</span>
|
||||
</FileUpload>
|
||||
</div>
|
||||
{selection.length > 0 && (
|
||||
<Button size="sm" variant="secondary" onClick={onClearSelection}>
|
||||
<Trans i18nKey="canvas.tree-navigation-editor.clear-selection">Clear selection</Trans>
|
||||
@@ -183,6 +193,12 @@ export const TreeNavigationEditor = ({ item }: StandardEditorProps<unknown, Tree
|
||||
const getStyles = (theme: GrafanaTheme2) => ({
|
||||
addLayerButton: css({
|
||||
marginLeft: '18px',
|
||||
minWidth: 'calc(min(100px, 0.05vw))',
|
||||
}),
|
||||
uploadFileButton: css({
|
||||
float: 'right',
|
||||
}),
|
||||
uploadFileButtonDiv: css({
|
||||
minWidth: '150px',
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { FrameState } from 'app/features/canvas/runtime/frame';
|
||||
import { Scene, SelectionParams } from 'app/features/canvas/runtime/scene';
|
||||
|
||||
import { AnchorPoint, ConnectionState, LineStyle, StrokeDasharray } from './types';
|
||||
// Remove import DxfParser from 'dxf-parser';
|
||||
|
||||
export function doSelect(scene: Scene, element: ElementState | FrameState) {
|
||||
try {
|
||||
@@ -417,3 +418,369 @@ export function removeStyles(styles: React.CSSProperties, target: HTMLDivElement
|
||||
target.style[key as any] = '';
|
||||
}
|
||||
}
|
||||
|
||||
// In onImportFile, simplify to only handle draw.io
|
||||
export async function onImportFile(target: EventTarget & HTMLInputElement, rootLayer?: FrameState) {
|
||||
if (target.files && target.files[0]) {
|
||||
const file = target.files[0];
|
||||
if (file.name.endsWith('.svg')) {
|
||||
handleSVGFile(file, rootLayer);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface SVGElementInfo {
|
||||
markup: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
// Convert foreignObject to SVG text element
|
||||
function convertForeignObjectToText(element: SVGForeignObjectElement, width: number, height: number): string {
|
||||
// Extract text content from nested divs
|
||||
const textContent = element.textContent?.trim() || '';
|
||||
|
||||
if (!textContent) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// Try to extract styling from nested divs
|
||||
let fontSize = '12px';
|
||||
let fontFamily = 'Helvetica, Arial, sans-serif';
|
||||
let fill = '#000000';
|
||||
let textAnchor = 'middle';
|
||||
let alignmentBaseline = 'middle';
|
||||
let fontWeight = 'normal';
|
||||
let fontStyle = 'normal';
|
||||
|
||||
// Look for style information in the foreignObject's children
|
||||
const divs = element.querySelectorAll('div');
|
||||
for (const div of Array.from(divs)) {
|
||||
const style = window.getComputedStyle(div);
|
||||
|
||||
if (style.fontSize && style.fontSize !== '0px') {
|
||||
fontSize = style.fontSize;
|
||||
}
|
||||
if (style.fontFamily && style.fontFamily !== 'none') {
|
||||
fontFamily = style.fontFamily.replace(/['"]/g, '');
|
||||
}
|
||||
if (style.color && style.color !== 'rgba(0, 0, 0, 0)') {
|
||||
fill = style.color;
|
||||
}
|
||||
if (style.fontWeight && parseInt(style.fontWeight, 10) >= 600) {
|
||||
fontWeight = 'bold';
|
||||
}
|
||||
if (style.fontStyle === 'italic') {
|
||||
fontStyle = 'italic';
|
||||
}
|
||||
|
||||
// Check text-align for text-anchor
|
||||
const textAlign = style.textAlign;
|
||||
if (textAlign === 'left' || textAlign === 'start') {
|
||||
textAnchor = 'start';
|
||||
} else if (textAlign === 'right' || textAlign === 'end') {
|
||||
textAnchor = 'end';
|
||||
} else if (textAlign === 'center') {
|
||||
textAnchor = 'middle';
|
||||
}
|
||||
|
||||
// Check align-items for vertical alignment
|
||||
const alignItems = style.alignItems;
|
||||
if (alignItems === 'flex-start' || alignItems === 'start') {
|
||||
alignmentBaseline = 'hanging';
|
||||
} else if (alignItems === 'flex-end' || alignItems === 'end') {
|
||||
alignmentBaseline = 'baseline';
|
||||
} else if (alignItems === 'center') {
|
||||
alignmentBaseline = 'middle';
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate text position within the element's local coordinate system
|
||||
// The text should be positioned relative to (0, 0) since we're creating a new SVG with its own viewBox
|
||||
let textX = 0;
|
||||
let textY = 0;
|
||||
|
||||
// Adjust horizontal position based on text anchor and width
|
||||
if (textAnchor === 'start') {
|
||||
textX = 0;
|
||||
} else if (textAnchor === 'middle') {
|
||||
textX = width / 2;
|
||||
} else if (textAnchor === 'end') {
|
||||
textX = width;
|
||||
}
|
||||
|
||||
// Adjust vertical position based on alignment and height
|
||||
// For better visual centering, we need to account for font metrics
|
||||
const fontSizeNum = parseFloat(fontSize);
|
||||
if (alignmentBaseline === 'hanging') {
|
||||
textY = 0;
|
||||
} else if (alignmentBaseline === 'middle') {
|
||||
// Use a slight offset to better center the text visually
|
||||
// SVG's dominant-baseline="middle" aligns to the mathematical middle,
|
||||
// but visually we want it slightly lower to account for descenders
|
||||
textY = height / 2 + fontSizeNum * 0.1;
|
||||
} else if (alignmentBaseline === 'baseline') {
|
||||
textY = height;
|
||||
}
|
||||
|
||||
// Create SVG text element with position and styling info
|
||||
return `<text x="${textX}" y="${textY}" font-family="${fontFamily}" font-size="${fontSize}" fill="${fill}" text-anchor="${textAnchor}" dominant-baseline="${alignmentBaseline}" font-weight="${fontWeight}" font-style="${fontStyle}">${textContent}</text>`;
|
||||
}
|
||||
|
||||
async function parseSVGToElements(svgText: string): Promise<SVGElementInfo[]> {
|
||||
const elements: SVGElementInfo[] = [];
|
||||
|
||||
const parser = new DOMParser();
|
||||
const doc = parser.parseFromString(svgText, 'image/svg+xml');
|
||||
const svg = doc.documentElement;
|
||||
|
||||
// Check for parsing errors
|
||||
const parserError = svg.querySelector('parsererror');
|
||||
if (parserError) {
|
||||
throw new Error('Failed to parse SVG: ' + parserError.textContent);
|
||||
}
|
||||
|
||||
// Create temporary SVG for measurements
|
||||
const tempSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
||||
tempSvg.setAttribute('width', svg.getAttribute('width') || '1000');
|
||||
tempSvg.setAttribute('height', svg.getAttribute('height') || '1000');
|
||||
tempSvg.setAttribute('viewBox', svg.getAttribute('viewBox') || '0 0 1000 1000');
|
||||
tempSvg.style.position = 'absolute';
|
||||
tempSvg.style.left = '-9999px';
|
||||
tempSvg.style.top = '-9999px';
|
||||
document.body.appendChild(tempSvg);
|
||||
|
||||
// Clone the entire SVG content into temp SVG for accurate measurements
|
||||
const clonedContent = svg.cloneNode(true);
|
||||
if (clonedContent instanceof SVGSVGElement) {
|
||||
// Copy all children from cloned SVG to temp SVG
|
||||
while (clonedContent.firstChild) {
|
||||
tempSvg.appendChild(clonedContent.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Recursively process all elements
|
||||
function processElement(element: Element, depth = 0): void {
|
||||
// Skip non-SVG elements
|
||||
if (!(element instanceof SVGElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip defs, metadata, and other non-visual elements
|
||||
const skipTags = ['defs', 'metadata', 'style', 'script', 'title', 'desc'];
|
||||
if (skipTags.includes(element.tagName.toLowerCase())) {
|
||||
return;
|
||||
}
|
||||
|
||||
const tagName = element.tagName.toLowerCase();
|
||||
|
||||
// Special handling for foreignObject - treat it as a leaf element even if it has children
|
||||
// because its children are HTML/text content, not SVG elements to be separated
|
||||
const isForeignObject = tagName === 'foreignobject';
|
||||
|
||||
// Check if this element has any visual SVG children (not applicable for foreignObject)
|
||||
const visualChildren = isForeignObject
|
||||
? []
|
||||
: Array.from(element.children).filter((child) => {
|
||||
if (!(child instanceof SVGElement)) {
|
||||
return false;
|
||||
}
|
||||
const childTag = child.tagName.toLowerCase();
|
||||
return !skipTags.includes(childTag);
|
||||
});
|
||||
|
||||
// If element has visual children, recurse into them instead of adding this element
|
||||
if (visualChildren.length > 0) {
|
||||
for (const child of visualChildren) {
|
||||
processElement(child, depth + 1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Special handling for foreignObject - convert to text
|
||||
if (isForeignObject && element instanceof SVGForeignObjectElement) {
|
||||
let x = parseFloat(element.getAttribute('x') || '0');
|
||||
let y = parseFloat(element.getAttribute('y') || '0');
|
||||
|
||||
// Don't parse percentage values as numbers - they'll be extracted from nested divs
|
||||
const widthAttrRaw = element.getAttribute('width') || '0';
|
||||
const heightAttrRaw = element.getAttribute('height') || '0';
|
||||
let width = widthAttrRaw.includes('%') ? 0 : parseFloat(widthAttrRaw);
|
||||
let height = heightAttrRaw.includes('%') ? 0 : parseFloat(heightAttrRaw);
|
||||
|
||||
// Handle percentage-based dimensions by extracting from nested div styles
|
||||
const widthAttr = element.getAttribute('width');
|
||||
const heightAttr = element.getAttribute('height');
|
||||
if (widthAttr?.includes('%') || heightAttr?.includes('%') || width === 0 || height === 0) {
|
||||
// Extract position and dimensions from nested div styles (draw.io uses margin-left, padding-top, width)
|
||||
const divs = element.querySelectorAll('div');
|
||||
for (const div of Array.from(divs)) {
|
||||
const style = window.getComputedStyle(div);
|
||||
|
||||
// Check for inline styles first (more accurate for draw.io)
|
||||
const inlineStyle = div.getAttribute('style') || '';
|
||||
|
||||
// Extract margin-left
|
||||
const marginLeftMatch = inlineStyle.match(/margin-left:\s*(\d+)px/);
|
||||
if (marginLeftMatch) {
|
||||
x = parseFloat(marginLeftMatch[1]);
|
||||
} else if (style.marginLeft && style.marginLeft !== '0px') {
|
||||
x = parseFloat(style.marginLeft);
|
||||
}
|
||||
|
||||
// Extract padding-top (used for vertical positioning in draw.io)
|
||||
const paddingTopMatch = inlineStyle.match(/padding-top:\s*(\d+)px/);
|
||||
if (paddingTopMatch) {
|
||||
y = parseFloat(paddingTopMatch[1]);
|
||||
} else if (style.paddingTop && style.paddingTop !== '0px') {
|
||||
y = parseFloat(style.paddingTop);
|
||||
}
|
||||
|
||||
// Extract width
|
||||
const widthMatch = inlineStyle.match(/width:\s*(\d+)px/);
|
||||
if (widthMatch) {
|
||||
width = parseFloat(widthMatch[1]);
|
||||
} else if (style.width && style.width !== 'auto' && !style.width.includes('%')) {
|
||||
width = parseFloat(style.width);
|
||||
}
|
||||
|
||||
// Extract height (often 1px in draw.io, we'll use font size as fallback)
|
||||
const heightMatch = inlineStyle.match(/height:\s*(\d+)px/);
|
||||
if (heightMatch) {
|
||||
const h = parseFloat(heightMatch[1]);
|
||||
if (h > 1) {
|
||||
height = h;
|
||||
}
|
||||
} else if (style.height && style.height !== 'auto' && !style.height.includes('%')) {
|
||||
const h = parseFloat(style.height);
|
||||
if (h > 1) {
|
||||
height = h;
|
||||
}
|
||||
}
|
||||
|
||||
// If we found position/size info, break
|
||||
if (x > 0 || y > 0 || width > 0) {
|
||||
// Use a reasonable default height if not specified or too small
|
||||
if (height <= 1) {
|
||||
const fontSize = parseFloat(style.fontSize || '12');
|
||||
height = fontSize * 1.5; // Line height approximation
|
||||
}
|
||||
|
||||
// Adjust y position - padding-top positions the container, but we need to account for
|
||||
// the text being vertically centered within that container
|
||||
// Subtract half the height to position the element so its center is at the padding-top position
|
||||
if (y > 0) {
|
||||
y = y - height / 2;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (width > 0 && height > 0) {
|
||||
const textElement = convertForeignObjectToText(element, width, height);
|
||||
if (textElement) {
|
||||
// Create SVG with converted text element - no transform needed since text is already in relative coords
|
||||
const shiftedMarkup = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}">${textElement}</svg>`;
|
||||
|
||||
elements.push({
|
||||
markup: shiftedMarkup,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// This is a leaf element - try to get bounding box
|
||||
let bbox: DOMRect | null = null;
|
||||
try {
|
||||
// Check if element is an SVGGraphicsElement which has getBBox
|
||||
if ('getBBox' in element && typeof element.getBBox === 'function') {
|
||||
bbox = element.getBBox();
|
||||
}
|
||||
} catch (e) {
|
||||
// Some elements don't support getBBox
|
||||
}
|
||||
|
||||
// If element has valid dimensions, add it
|
||||
if (bbox && bbox.width > 0 && bbox.height > 0) {
|
||||
// Create shifted markup with proper viewBox
|
||||
const shiftedMarkup = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${bbox.width} ${bbox.height}"><g transform="translate(${-bbox.x} ${-bbox.y})">${element.outerHTML}</g></svg>`;
|
||||
|
||||
elements.push({
|
||||
markup: shiftedMarkup,
|
||||
x: bbox.x,
|
||||
y: bbox.y,
|
||||
width: bbox.width,
|
||||
height: bbox.height,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Start processing from the root
|
||||
for (const child of Array.from(tempSvg.children)) {
|
||||
processElement(child);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
document.body.removeChild(tempSvg);
|
||||
|
||||
console.log('Parsed SVG elements:', elements);
|
||||
return elements;
|
||||
}
|
||||
|
||||
export async function handleSVGFile(file: File, rootLayer?: FrameState) {
|
||||
const fileText = await file.text();
|
||||
try {
|
||||
const elements = await parseSVGToElements(fileText);
|
||||
|
||||
if (rootLayer && elements.length > 0) {
|
||||
// Get the SVG element item from registry
|
||||
const svgItem = canvasElementRegistry.getIfExists('svg');
|
||||
if (!svgItem) {
|
||||
console.error('SVG element type not found in registry');
|
||||
return;
|
||||
}
|
||||
|
||||
// Add each parsed element to the canvas
|
||||
for (const element of elements) {
|
||||
const newElementOptions: CanvasElementOptions = {
|
||||
...svgItem.getNewOptions(),
|
||||
type: 'svg',
|
||||
name: rootLayer.scene.getNextElementName(),
|
||||
placement: {
|
||||
top: element.y,
|
||||
left: element.x,
|
||||
width: element.width,
|
||||
height: element.height,
|
||||
rotation: 0,
|
||||
},
|
||||
config: {
|
||||
content: {
|
||||
mode: 'fixed' as const,
|
||||
fixed: element.markup,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const newElement = new ElementState(svgItem, newElementOptions, rootLayer);
|
||||
newElement.updateData(rootLayer.scene.context);
|
||||
rootLayer.elements.push(newElement);
|
||||
}
|
||||
|
||||
// Save and update the scene
|
||||
rootLayer.scene.save();
|
||||
rootLayer.reinitializeMoveable();
|
||||
|
||||
console.log(`Added ${elements.length} SVG elements to canvas`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing SVG file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user