我正在尝试将 svg 路径转换为 javascript 中的 svg 多边形。我发现这个函数可以沿着路径爬行并提取其坐标。
var length = path.getTotalLength();
var p=path.getPointAtLength(0);
var stp=p.x+","+p.y;
for(var i=1; i<length; i++){
p=path.getPointAtLength(i);
stp=stp+" "+p.x+","+p.y;
}
这可行,但对于最初只有六个点的多边形,它会返回数百个点。我怎样才能只得到必要的点(所有路径都是直线,没有曲线)
好的明白了.. 函数 getPathSegAtLength() 返回实际路径段的编号。这样就很容易了。
var len = path.getTotalLength();
var p=path.getPointAtLength(0);
var seg = path.getPathSegAtLength(0);
var stp=p.x+","+p.y;
for(var i=1; i<len; i++){
p=path.getPointAtLength(i);
if (path.getPathSegAtLength(i)>seg) {
stp=stp+" "+p.x+","+p.y;
seg = path.getPathSegAtLength(i);
}
}
path.getPointAtLength()
适用于不需要速度和质量的粗略目的。如果你得到每个像素,你会得到数千个点,但质量仍然很低,因为 SVG 路径可以有十进制值,例如。 0.1、0.2。
如果您想要更精确,请致电例如。
path.getPointAtLength(0.1)
您可以轻松获得复杂路径中的数万个点,并且过程持续几秒或几十秒。之后,您必须减少点的计数(https://stackoverflow.com/a/15976155/1691517),这又持续了几秒钟。但如果错误的点被删除,质量仍然会很低。
更好的技术是首先将所有路径段转换为三次曲线,例如。使用Raphael'spath2curve(),然后使用一些自适应方法(http://antigrain.com/research/adaptive_bezier/)将立方线段转换为点,你可以同时获得速度和质量。之后就不需要减分了,因为自适应过程本身就有参数来调整质量。
我已经制作了一个可以完成所有这些操作的函数,当它对速度进行了足够优化时我将发布它。经过数千条随机路径测试后,质量和可靠性似乎是 100%,而且速度比
path.getPointAtLength()
快得多。
要迭代段,请使用如下内容:
var segList = path.normalizedPathSegList; // or .pathSegList
for(var i=1; i<segList.numberOfSegments; i++){
var seg = segList.getItem(i);
}
如果您想减少顶点数量,那么您可以使用Simplify.js,如此处所述。
我怎样才能只得到必要的点(所有路径都是直的 直线,无曲线)
如果您的路径实际上只包含 linetos,您可以采取捷径通过解析路径数据来检索多边形顶点。
此方法需要:
d
属性解析路径数据h
、v
转换为其普通写法 l
等效项我们可以通过检查路径的
d
属性来检查路径是否可以表示为多边形,如下所示:
function pathIsPolygon(d) {
// any beziers or arc commands?
let isPolygon = /[csqta]/gi.test(d) ? false : true
return isPolygon;
}
我们基本上是在测试路径数据字符串是否包含任何贝塞尔或圆弧命令。如果没有(没有
c
、s
、q
、t
、a
命令):我们可以通过获取表示最终路径点的最后几个值来继续从路径数据中检索顶点.
/**
* 1. is polygon:
*/
let path = document.getElementById("path");
let poly = document.getElementById("poly");
//let vertices = getPathPolygonVertices(path, split, decimals);
// parse pathdata
let d = path.getAttribute("d");
let pathData = parsePathDataNormalized(d);
/**
* check if path is already a polygon:
* just return the final command points
*/
let vertices;
let isPolygon = pathIsPolygon(d);
if (isPolygon) {
console.log(path.id, "is polygon");
vertices = getPathDataVertices(pathData);
}
//apply
let ptAtt = vertices
.map((pt) => {
return Object.values(pt);
})
.flat()
.join(" ");
poly.setAttribute("points", ptAtt);
//output vertices/point array
verticesOut.value = JSON.stringify(vertices, null, ' ');
function pathIsPolygon(d) {
// any beziers or arc commands?
let isPolygon = /[csqta]/gi.test(d) ? false : true;
return isPolygon;
}
function getPathDataVertices(pathData) {
let polyPoints = [];
pathData.forEach((com) => {
let values = com.values;
// get final on path point from last 2 values
if (values.length) {
let pt = {
x: values[values.length - 2],
y: values[values.length - 1]
};
polyPoints.push(pt);
}
});
return polyPoints;
}
/**
* Standalone pathData parser
* including normalization options
* returns a pathData array compliant
* with the w3C SVGPathData interface draft
* https://svgwg.org/specs/paths/#InterfaceSVGPathData
*/
function parsePathDataNormalized(d) {
d = d
// remove new lines, tabs an comma with whitespace
.replace(/[\n\r\t|,]/g, " ")
// pre trim left and right whitespace
.trim()
// add space before minus sign
.replace(/(\d)-/g, "$1 -")
// decompose multiple adjacent decimal delimiters like 0.5.5.5 => 0.5 0.5 0.5
.replace(/(\.)(?=(\d+\.\d+)+)(\d+)/g, "$1$3 ");
let pathData = [];
let cmdRegEx = /([mlcqazvhst])([^mlcqazvhst]*)/gi;
let commands = d.match(cmdRegEx);
// valid command value lengths
let comLengths = {m: 2,a: 7,c: 6,h: 1,l: 2,q: 4,s: 4,t: 2,v: 1,z: 0
};
// offsets for absolute conversion
let offX, offY, lastX, lastY;
for (let c = 0; c < commands.length; c++) {
let com = commands[c];
let type = com.substring(0, 1);
let typeRel = type.toLowerCase();
let typeAbs = type.toUpperCase();
let isRel = type === typeRel;
let chunkSize = comLengths[typeRel];
// split values to array
let values = com.substring(1, com.length).trim().split(" ").filter(Boolean);
/**
* A - Arc commands
* large arc and sweep flags
* are boolean and can be concatenated like
* 11 or 01
* or be concatenated with the final on path points like
* 1110 10 => 1 1 10 10
*/
if (typeRel === "a" && values.length != comLengths.a) {
let n = 0,
arcValues = [];
for (let i = 0; i < values.length; i++) {
let value = values[i];
// reset counter
if (n >= chunkSize) {
n = 0;
}
// if 3. or 4. parameter longer than 1
if ((n === 3 || n === 4) && value.length > 1) {
let largeArc = n === 3 ? value.substring(0, 1) : "";
let sweep = n === 3 ? value.substring(1, 2) : value.substring(0, 1);
let finalX = n === 3 ? value.substring(2) : value.substring(1);
let comN = [largeArc, sweep, finalX].filter(Boolean);
arcValues.push(comN);
n += comN.length;
} else {
// regular
arcValues.push(value);
n++;
}
}
values = arcValues.flat().filter(Boolean);
}
// string to number
values = values.map(Number);
// if string contains repeated shorthand commands - split them
let hasMultiple = values.length > chunkSize;
let chunk = hasMultiple ? values.slice(0, chunkSize) : values;
let comChunks = [{
type: type,
values: chunk
}];
// has implicit or repeated commands – split into chunks
if (hasMultiple) {
let typeImplicit = typeRel === "m" ? (isRel ? "l" : "L") : type;
for (let i = chunkSize; i < values.length; i += chunkSize) {
let chunk = values.slice(i, i + chunkSize);
comChunks.push({
type: typeImplicit,
values: chunk
});
}
}
/**
* convert to absolute
* init offset from 1st M
*/
if (c === 0) {
offX = values[0];
offY = values[1];
lastX = offX;
lastY = offY;
}
let typeFirst = comChunks[0].type;
typeAbs = typeFirst.toUpperCase();
// first M is always absolute
isRel =
typeFirst.toLowerCase() === typeFirst && pathData.length ? true : false;
for (let i = 0; i < comChunks.length; i++) {
let com = comChunks[i];
let type = com.type;
let values = com.values;
let valuesL = values.length;
let comPrev = comChunks[i - 1] ?
comChunks[i - 1] :
c > 0 && pathData[pathData.length - 1] ?
pathData[pathData.length - 1] :
comChunks[i];
let valuesPrev = comPrev.values;
let valuesPrevL = valuesPrev.length;
isRel =
comChunks.length > 1 ?
type.toLowerCase() === type && pathData.length :
isRel;
if (isRel) {
com.type = comChunks.length > 1 ? type.toUpperCase() : typeAbs;
switch (typeRel) {
case "a":
com.values = [
values[0],
values[1],
values[2],
values[3],
values[4],
values[5] + offX,
values[6] + offY
];
break;
case "h":
case "v":
com.values = type === "h" ? [values[0] + offX] : [values[0] + offY];
break;
case "m":
case "l":
case "t":
com.values = [values[0] + offX, values[1] + offY];
break;
case "c":
com.values = [
values[0] + offX,
values[1] + offY,
values[2] + offX,
values[3] + offY,
values[4] + offX,
values[5] + offY
];
break;
case "q":
case "s":
com.values = [
values[0] + offX,
values[1] + offY,
values[2] + offX,
values[3] + offY
];
break;
}
}
// is absolute
else {
offX = 0;
offY = 0;
}
/**
* convert shorthands
*/
let shorthandTypes = ["H", "V", "S", "T"];
if (shorthandTypes.includes(typeAbs)) {
let cp1X, cp1Y, cpN1X, cpN1Y, cp2X, cp2Y;
if (com.type === "H" || com.type === "V") {
com.values =
com.type === "H" ? [com.values[0], lastY] : [lastX, com.values[0]];
com.type = "L";
} else if (com.type === "T" || com.type === "S") {
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[cp2X, cp2Y] =
valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];
// new control point
cpN1X = com.type === "T" ? lastX * 2 - cp1X : lastX * 2 - cp2X;
cpN1Y = com.type === "T" ? lastY * 2 - cp1Y : lastY * 2 - cp2Y;
com.values = [cpN1X, cpN1Y, com.values].flat();
com.type = com.type === "T" ? "Q" : "C";
}
}
// add to pathData array
pathData.push(com);
// update offsets
lastX =
valuesL > 1 ?
values[valuesL - 2] + offX :
typeRel === "h" ?
values[0] + offX :
lastX;
lastY =
valuesL > 1 ?
values[valuesL - 1] + offY :
typeRel === "v" ?
values[0] + offY :
lastY;
offX = lastX;
offY = lastY;
}
}
/**
* first M is always absolute/uppercase -
* unless it adds relative linetos
* (facilitates d concatenating)
*/
pathData[0].type = "M";
return pathData;
}
svg {
overflow: visible;
width:25%;
}
svg path {
stroke-width: 2%;
stroke: #ccc;
}
svg polygon {
stroke-width: 0.75%;
marker-start: url(#markerStart);
marker-mid: url(#markerRound);
}
textarea {
width: 100%;
display: block;
min-height: 10em;
}
<h3>Path is already a polygon</h3>
<svg id="svg" viewBox="10 10 94 80">
<path id="path" fill="none" stroke="black"
d="m104 33.4-6.9-16.6-16.6-6.8-16.6 6.8-6.9 16.6-6.9-16.6-16.6-6.8-16.6 6.8-6.9 16.6 9.4 23 17.5 18.3 20.1 15.3 20.1-15.3 17.5-18.3z" />
<polygon id="poly" points="" fill="none" stroke="red" />
</svg>
<h3>Point/vertices data</h3>
<textarea id="verticesOut"></textarea>
<!-- markers to show commands -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;opacity:0">
<defs>
<marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth"
markerWidth="10" markerHeight="10" orient="auto-start-reverse">
<circle cx="5" cy="5" r="5" fill="green"></circle>
<marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5"
markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
<circle cx="5" cy="5" r="2.5" fill="red"></circle>
</marker>
</defs>
</svg>
通过解析路径数据,我们可以轻松地从每个命令检索 x 和 y 坐标,以创建点对象数组。
解析是通过自定义解析脚本完成的,尝试符合建议的 W3C 路径数据接口 。该草案旨在取代(大部分)已弃用/不受支持的
SVGPathSeg
接口。
值得注意的是,仍然有一个 Polyfill。请参阅 “https://github.com/progers/pathseg” .
您实际上可以使用任何路径数据解析器(只要它提供了一种将命令转换为所有绝对命令和“非短”命令的方法,例如
h
和v
到l
),例如Jarek Foksa的路径数据- polyfill 像这样设置标准化参数path.getPathData({normalize:true})
。
如果路径还包含曲线,
getPointAtlength()
可能并不理想,因为它不会保留基本几何形状 - 尊重命令最终点。
尽管点是根据相等的路径长度间隔计算的,但视觉结果似乎相当任意。我们失去了原始形状的预期对称性。
我们可以通过根据每个线段的长度添加顶点来解决这个问题。
let decimals = 2;
let split = 32;
/**
* 1. is polygon:
*/
let path = document.getElementById('path')
let poly = document.getElementById('poly')
let vertices = getPathPolygonVertices(path, split, decimals);
//apply
let ptAtt = vertices.map(pt => {
return Object.values(pt)
}).flat().join(' ')
poly.setAttribute('points', ptAtt)
/**
* 2. has curves:
*/
let path1 = document.getElementById('path1')
let poly1 = document.getElementById('poly1')
let vertices1 = getPathPolygonVertices(path1, split, decimals);
//apply
let ptAtt1 = vertices1.map(pt => {
return Object.values(pt)
}).flat().join(' ')
poly1.setAttribute('points', ptAtt1)
function getPathPolygonVertices(path, split = 16, decimals = 3) {
let pts = []
let ns = 'http://www.w3.org/2000/svg'
// parse pathdata
let d = path.getAttribute('d')
let pathData = parsePathDataNormalized(d)
/**
* check if path is already polygon:
* just return the final command points
*/
let isPolygon = pathIsPolygon(d);
if (isPolygon) {
console.log(path.id, 'is polygon');
pts = getPathDataVertices(pathData)
return pts
}
// target side length
let totalLength = path.getTotalLength();
let step = totalLength / split;
let lastLength = 0;
let Mvalues = pathData[0].values;
let M = {
x: Mvalues[Mvalues.length - 2],
y: Mvalues[Mvalues.length - 1]
};
for (let i = 1; i < pathData.length; i++) {
let com = pathData[i];
let comPrev = pathData[i - 1];
let type = com.type.toLowerCase();
let [values, valuesPrev] = [com.values, comPrev.values];
//previous commands final point
let p0 = {
x: valuesPrev[valuesPrev.length - 2],
y: valuesPrev[valuesPrev.length - 1]
};
let p = values.length ? {
x: values[values.length - 2],
y: values[values.length - 1]
} : p0;
if (values.length) {
// create temporary path to get segment length
let pathSeg = document.createElementNS(ns, 'path')
pathSeg.setAttribute('d', `M ${p0.x} ${p0.y} ${com.type} ${com.values.join(' ')}`)
let segLength = pathSeg.getTotalLength()
// fit to segment length – keep command end points to better retain shape
let segSplits = Math.ceil(segLength / step);
// if lineto: no need to calculate points
if (type === 'l') {
pts.push(p0);
pts.push(p);
} else {
for (let s = 0; s < segSplits; s++) {
let len = lastLength + (segLength / segSplits) * s;
// get point
let pt = path.getPointAtLength(len);
pts.push(pt);
}
}
//remove temorary path
pathSeg.remove()
lastLength += segLength;
}
// is Z/closepath: add previous end point
else {
pts.push(p0);
}
}
//round coordinates
pts = Array.from(pts).map(pt => {
return {
x: +pt.x.toFixed(decimals),
y: y = +pt.y.toFixed(decimals)
}
});
return pts
}
function pathIsPolygon(d) {
// any beziers or arc commands?
let isPolygon = /[csqta]/gi.test(d) ? false : true
return isPolygon;
}
function getPathDataVertices(pathData) {
let polyPoints = [];
pathData.forEach(com => {
let values = com.values;
// get final on path point from last 2 values
if (values.length) {
let pt = {
x: values[values.length - 2],
y: values[values.length - 1]
}
polyPoints.push(pt)
}
})
return polyPoints
}
/**
* Standalone pathData parser
* including normalization options
* returns a pathData array compliant
* with the w3C SVGPathData interface draft
* https://svgwg.org/specs/paths/#InterfaceSVGPathData
*/
function parsePathDataNormalized(d) {
d = d
// remove new lines, tabs an comma with whitespace
.replace(/[\n\r\t|,]/g, " ")
// pre trim left and right whitespace
.trim()
// add space before minus sign
.replace(/(\d)-/g, "$1 -")
// decompose multiple adjacent decimal delimiters like 0.5.5.5 => 0.5 0.5 0.5
.replace(/(\.)(?=(\d+\.\d+)+)(\d+)/g, "$1$3 ");
let pathData = [];
let cmdRegEx = /([mlcqazvhst])([^mlcqazvhst]*)/gi;
let commands = d.match(cmdRegEx);
// valid command value lengths
let comLengths = {
m: 2,
a: 7,
c: 6,
h: 1,
l: 2,
q: 4,
s: 4,
t: 2,
v: 1,
z: 0
};
// offsets for absolute conversion
let offX, offY, lastX, lastY;
for (let c = 0; c < commands.length; c++) {
let com = commands[c];
let type = com.substring(0, 1);
let typeRel = type.toLowerCase();
let typeAbs = type.toUpperCase();
let isRel = type === typeRel;
let chunkSize = comLengths[typeRel];
// split values to array
let values = com.substring(1, com.length).trim().split(" ").filter(Boolean);
/**
* A - Arc commands
* large arc and sweep flags
* are boolean and can be concatenated like
* 11 or 01
* or be concatenated with the final on path points like
* 1110 10 => 1 1 10 10
*/
if (typeRel === "a" && values.length != comLengths.a) {
let n = 0,
arcValues = [];
for (let i = 0; i < values.length; i++) {
let value = values[i];
// reset counter
if (n >= chunkSize) {
n = 0;
}
// if 3. or 4. parameter longer than 1
if ((n === 3 || n === 4) && value.length > 1) {
let largeArc = n === 3 ? value.substring(0, 1) : "";
let sweep = n === 3 ? value.substring(1, 2) : value.substring(0, 1);
let finalX = n === 3 ? value.substring(2) : value.substring(1);
let comN = [largeArc, sweep, finalX].filter(Boolean);
arcValues.push(comN);
n += comN.length;
} else {
// regular
arcValues.push(value);
n++;
}
}
values = arcValues.flat().filter(Boolean);
}
// string to number
values = values.map(Number);
// if string contains repeated shorthand commands - split them
let hasMultiple = values.length > chunkSize;
let chunk = hasMultiple ? values.slice(0, chunkSize) : values;
let comChunks = [{
type: type,
values: chunk
}];
// has implicit or repeated commands – split into chunks
if (hasMultiple) {
let typeImplicit = typeRel === "m" ? (isRel ? "l" : "L") : type;
for (let i = chunkSize; i < values.length; i += chunkSize) {
let chunk = values.slice(i, i + chunkSize);
comChunks.push({
type: typeImplicit,
values: chunk
});
}
}
/**
* convert to absolute
* init offset from 1st M
*/
if (c === 0) {
offX = values[0];
offY = values[1];
lastX = offX;
lastY = offY;
}
let typeFirst = comChunks[0].type;
typeAbs = typeFirst.toUpperCase();
// first M is always absolute
isRel =
typeFirst.toLowerCase() === typeFirst && pathData.length ? true : false;
for (let i = 0; i < comChunks.length; i++) {
let com = comChunks[i];
let type = com.type;
let values = com.values;
let valuesL = values.length;
let comPrev = comChunks[i - 1] ?
comChunks[i - 1] :
c > 0 && pathData[pathData.length - 1] ?
pathData[pathData.length - 1] :
comChunks[i];
let valuesPrev = comPrev.values;
let valuesPrevL = valuesPrev.length;
isRel =
comChunks.length > 1 ?
type.toLowerCase() === type && pathData.length :
isRel;
if (isRel) {
com.type = comChunks.length > 1 ? type.toUpperCase() : typeAbs;
switch (typeRel) {
case "a":
com.values = [
values[0],
values[1],
values[2],
values[3],
values[4],
values[5] + offX,
values[6] + offY
];
break;
case "h":
case "v":
com.values = type === "h" ? [values[0] + offX] : [values[0] + offY];
break;
case "m":
case "l":
case "t":
com.values = [values[0] + offX, values[1] + offY];
break;
case "c":
com.values = [
values[0] + offX,
values[1] + offY,
values[2] + offX,
values[3] + offY,
values[4] + offX,
values[5] + offY
];
break;
case "q":
case "s":
com.values = [
values[0] + offX,
values[1] + offY,
values[2] + offX,
values[3] + offY
];
break;
}
}
// is absolute
else {
offX = 0;
offY = 0;
}
/**
* convert shorthands
*/
let shorthandTypes = ["H", "V", "S", "T"];
if (shorthandTypes.includes(typeAbs)) {
let cp1X, cp1Y, cpN1X, cpN1Y, cp2X, cp2Y;
if (com.type === "H" || com.type === "V") {
com.values =
com.type === "H" ? [com.values[0], lastY] : [lastX, com.values[0]];
com.type = "L";
} else if (com.type === "T" || com.type === "S") {
[cp1X, cp1Y] = [valuesPrev[0], valuesPrev[1]];
[cp2X, cp2Y] =
valuesPrevL > 2 ? [valuesPrev[2], valuesPrev[3]] : [valuesPrev[0], valuesPrev[1]];
// new control point
cpN1X = com.type === "T" ? lastX * 2 - cp1X : lastX * 2 - cp2X;
cpN1Y = com.type === "T" ? lastY * 2 - cp1Y : lastY * 2 - cp2Y;
com.values = [cpN1X, cpN1Y, com.values].flat();
com.type = com.type === "T" ? "Q" : "C";
}
}
// add to pathData array
pathData.push(com);
// update offsets
lastX =
valuesL > 1 ?
values[valuesL - 2] + offX :
typeRel === "h" ?
values[0] + offX :
lastX;
lastY =
valuesL > 1 ?
values[valuesL - 1] + offY :
typeRel === "v" ?
values[0] + offY :
lastY;
offX = lastX;
offY = lastY;
}
}
pathData[0].type = "M";
return pathData;
}
svg {
border: 1px solid #ccc;
overflow: visible;
padding: 1em
}
.grd {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1em;
}
path {
stroke: #ccc;
stroke-width: 1.5%;
}
polygon {
marker-start: url(#markerStart);
marker-mid: url(#markerRound);
stroke-width: 0.5%;
}
<div class="grd">
<div class="col">
<h3>Path is already a polygon – skip more expensive calulations</h3>
<svg id="svg" viewBox="10 10 94 80">
<path id="path" fill="none" stroke="black" d="m104 33.4-6.9-16.6-16.6-6.8-16.6 6.8-6.9 16.6-6.9-16.6-16.6-6.8-16.6 6.8-6.9 16.6 9.4 23 17.5 18.3 20.1 15.3 20.1-15.3 17.5-18.3z" />
<polygon id="poly" points="" fill="none" stroke="red" />
</svg>
</div>
<div class="col">
<h3>Path has curves – retain on-path final points</h3>
<svg id="svg1" viewBox="0 0 100 85">
<path id="path1" fill="none" stroke="black" d="m50 85.2 33.4-27.8c9.9-9.9 16.6-17.9 16.6-32.4 0-13.7-11.2-24.9-25-24.9s-25 11.2-25 24.9c0-13.7-11.2-24.9-25-24.9s-25 11.2-25 24.9c0 16.6 8.7 24.5 16.6 32.4z" />
<polygon id="poly1" points="" fill="none" stroke="red" />
</svg>
</div>
</div>
<!-- markers to show commands -->
<svg id="svgMarkers" style="width:0; height:0; position:absolute; z-index:-1;float:left;opacity:0">
<defs>
<marker id="markerStart" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5" markerUnits="strokeWidth"
markerWidth="10" markerHeight="10" orient="auto-start-reverse">
<circle cx="5" cy="5" r="5" fill="green"></circle>
<marker id="markerRound" overflow="visible" viewBox="0 0 10 10" refX="5" refY="5"
markerUnits="strokeWidth" markerWidth="10" markerHeight="10" orient="auto-start-reverse">
<circle cx="5" cy="5" r="2.5" fill="red"></circle>
</marker>
</defs>
</svg>
l
行,我们将省略任何拆分