OK, try this. EDIT: fixed rounded rectangles – cannot use the “RoundedRectangle” figure generator.
<!DOCTYPE html>
<html>
<head>
<title>Simple SVG editor</title>
<!-- Copyright 1998-2024 by Northwoods Software Corporation. -->
</head>
<body>
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:400px"></div>
<button id="mySaveButton">Show and Save SVG</button><button id="myLoadButton">Load SVG</button>
<div id="mySvg"></div>
<textarea id="mySavedSvg" style="width:100%;height:250px"></textarea>
<script src="https://unpkg.com/gojs"></script>
<script id="code">
const myDiagram =
new go.Diagram("myDiagramDiv", {
"animationManager.isInitial": false,
"undoManager.isEnabled": true
});
myDiagram.nodeTemplate =
new go.Part({
locationSpot: go.Spot.Center,
resizable: true, resizeObjectName: "SHAPE",
rotatable: true, rotationSpot: go.Spot.Center
})
.bindTwoWay("location", "location", go.Point.parse, go.Point.stringifyFixed(1))
.bindTwoWay("angle")
.add(
new go.Shape({ name: "SHAPE" })
.bind("stroke")
.bind("strokeWidth")
.bind("fill")
.bind("figure") // only for rectangles or ellipses: "Rectangle" or "RR" or "Ellipse" or "Circle"
.bind("parameter1") // only for rounded rectangles
.bind("parameter2") // only for rounded rectangles
.bind("geometryString", "d") // for all other geometries
.bindTwoWay("desiredSize", "size", go.Size.parse, go.Size.stringifyFixed(3))
)
// Cannot use the "RoundedRectangle" predefined figure, because an SVG <rect> with corners is different
function svgRoundedRectGen(shp, w, h) {
let param1 = shp ? shp.parameter1 : 10;
if (isNaN(param1)) param1 = 10;
let param2 = shp ? shp.parameter2 : 10;
if (isNaN(param2)) param2 = 10;
const rx = Math.min(param1, w/2); // limit corner
const ry = Math.min(param2, h/2); // limit corner
const offx = rx * (1 - 4 * ((Math.sqrt(2) - 1) / 3));
const offy = ry * (1 - 4 * ((Math.sqrt(2) - 1) / 3));
const fig = new go.PathFigure(rx, 0, true);
if (rx < w/2) fig.add(new go.PathSegment(go.SegmentType.Line, w - rx, 0));
fig.add(new go.PathSegment(go.SegmentType.Bezier, w, ry, w - offx, 0, w, offy));
if (ry < h/2) fig.add(new go.PathSegment(go.SegmentType.Line, w, h - ry));
fig.add(new go.PathSegment(go.SegmentType.Bezier, w - rx, h, w, h - offy, w - offx, h));
if (rx < w/2) fig.add(new go.PathSegment(go.SegmentType.Line, rx, h));
fig.add(new go.PathSegment(go.SegmentType.Bezier, 0, h - ry, offx, h, 0, h - offy));
if (ry < h/2) fig.add(new go.PathSegment(go.SegmentType.Line, 0, ry));
fig.add(new go.PathSegment(go.SegmentType.Bezier, rx, 0, 0, offy, offx, 0).close());
return new go.Geometry().add(fig);
}
go.Shape.defineFigureGenerator("RR", svgRoundedRectGen);
// This only handles simple SVG shapes as immediate children of the <svg> element.
// It ignores <g> elements and any transforms.
function load(svgstr) {
const safeParse = (str, def) => {
const val = parseFloat(str);
if (isNaN(val)) return def !== undefined ? def : 0;
return val;
};
// add each SVG shape as a separate GoJS Part holding a GoJS Shape
const xmldoc = new DOMParser().parseFromString(svgstr, 'text/xml');
const nodeDataArray = [];
const roundedShape = new go.Shape("RR");
for (const elt of xmldoc.children[0].children) {
// represent each SVG path by a Shape with Geometry type Path with its own fill and stroke
const nodedata = {};
const id = elt.getAttribute('id');
if (typeof id === 'string') nodedata.key = id;
const tag = elt.tagName.toLowerCase();
nodedata.tag = tag;
let stroke = elt.getAttribute('stroke');
if (typeof stroke !== 'string' || stroke === 'none') stroke = null;
nodedata.stroke = stroke;
const sw = safeParse(elt.getAttribute('stroke-width'), stroke ? 1 : 0);
nodedata.strokeWidth = sw;
let fill = elt.getAttribute('fill');
if (typeof fill !== 'string' || fill === 'none') fill = null;
nodedata.fill = fill;
let geo = null;
let filled = true;
switch (tag) {
case "path": {
// convert the path data string into a go.Geometry
const pdata = elt.getAttribute('d');
if (typeof pdata === 'string') {
geo = go.Geometry.parse(pdata, true); // filled
}
break;
}
case "rect": {
const x = safeParse(elt.getAttribute('x'), 0);
const y = safeParse(elt.getAttribute('y'), 0);
const w = safeParse(elt.getAttribute('width'), 10);
const h = safeParse(elt.getAttribute('height'), 10);
let rx = safeParse(elt.getAttribute('rx'), 0);
let ry = safeParse(elt.getAttribute('ry'), 0);
if (rx > 0 || ry > 0) {
if (rx === 0) rx = ry;
else if (ry === 0) ry = rx;
nodedata.location = go.Point.stringify(new go.Point(x + w/2, y + h/2));
nodedata.size = go.Size.stringify(new go.Size(w, h));
nodedata.figure = "RR";
nodedata.parameter1 = rx;
nodedata.parameter2 = ry;
} else {
nodedata.location = go.Point.stringify(new go.Point(x + w/2, y + h/2));
nodedata.size = go.Size.stringify(new go.Size(w, h));
nodedata.figure = "Rectangle";
}
break;
}
case "ellipse": {
const cx = safeParse(elt.getAttribute('cx'), 0);
const cy = safeParse(elt.getAttribute('cy'), 0);
const rx = safeParse(elt.getAttribute('rx'), 10);
const ry = safeParse(elt.getAttribute('ry'), 10);
nodedata.location = go.Point.stringify(new go.Point(cx - rx, cy - ry));
nodedata.size = go.Size.stringify(new go.Size(2*rx, 2*ry));
nodedata.figure = "Ellipse";
break;
}
case "circle": {
const cx = safeParse(elt.getAttribute('cx'), 0);
const cy = safeParse(elt.getAttribute('cy'), 0);
const r = safeParse(elt.getAttribute('r'), 10);
nodedata.location = go.Point.stringify(new go.Point(cx - r, cy - r));
nodedata.size = go.Size.stringify(new go.Size(2*r, 2*r));
nodedata.figure = "Circle";
break;
}
case "line": {
filled = false;
const x1 = safeParse(elt.getAttribute('x1'), 0);
const y1 = safeParse(elt.getAttribute('y1'), 0);
const x2 = safeParse(elt.getAttribute('x2'), 10);
const y2 = safeParse(elt.getAttribute('y2'), 10);
geo = new go.Geometry(go.GeometryType.Line);
geo.startX = x1;
geo.startY = y1;
geo.endX = x2;
geo.endY = y2;
break;
}
case "polyline":
case "polygon": {
const pdata = elt.getAttribute('points');
if (typeof pdata === 'string') {
const a = pdata.split(/[\s,]/).filter(s => !!s); //??? is this parsing incomplete?
const fig = new go.PathFigure();
if (a.length >= 2) {
fig.startX = safeParse(a[0], 0);
fig.startY = safeParse(a[1], 0);
if (a.length > 3) {
for (let i = 2; i < a.length; i += 2) {
const seg = new go.PathSegment(go.SegmentType.Line, safeParse(a[i], 0), safeParse(a[i+1], 0));
fig.add(seg);
}
}
geo = new go.Geometry().add(fig);
}
if (tag === "polygon") {
const seg = fig.segments.last();
if (seg) seg.close();
} else {
filled = false;
}
}
break;
}
default: // ignore any elements that are not SVG shapes, especially <g>
break;
}
if (geo) {
const offset = geo.normalize();
nodedata.location = go.Point.stringify(new go.Point(-offset.x + geo.bounds.width/2, -offset.y + geo.bounds.height/2));
const pathstr = go.Geometry.stringifyFixed(3)(geo);
nodedata.d = filled ? go.Geometry.fillPath(pathstr) : pathstr;
}
nodeDataArray.push(nodedata);
}
myDiagram.model = new go.Model(nodeDataArray);
}
function save() {
const NS = "http://www.w3.org/2000/svg";
const svgnode = document.createElementNS(NS, "svg");
const db = myDiagram.documentBounds;
svgnode.setAttribute("width", db.width);
svgnode.setAttribute("height", db.height);
svgnode.setAttribute("viewBox", `${db.x} ${db.y} ${db.width} ${db.height}`);
svgnode.setAttribute("fill", "none"); // default no fill
myDiagram.parts.each(part => {
const data = part.data;
const elt = document.createElementNS(NS, "path");
if (data.stroke !== undefined && data.stroke !== null) elt.setAttribute("stroke", data.stroke);
elt.setAttribute("stroke-width", data.strokeWidth || 0);
if (data.fill !== undefined && data.fill !== null) elt.setAttribute("fill", data.fill);
const shape = part.findObject("SHAPE");
const geo = shape.geometry.copy();
const gb = geo.bounds.copy();
geo.rotate(part.angle, gb.width/2, gb.height/2);
geo.offset(part.location.x - gb.width/2, part.location.y - gb.height/2);
let pathstr = geo.toString();
if (pathstr[0] === "F") pathstr = pathstr.substring(2);
elt.setAttribute("d", pathstr);
svgnode.appendChild(elt);
});
const str = new XMLSerializer().serializeToString(svgnode);
document.getElementById("mySvg").innerHTML = str;
document.getElementById("mySavedSvg").value = str;
return str;
}
load(`<svg width="473" height="295" viewBox="-5 -75 473 295" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M130.461 164.079C130.461 179.16 118.541 191.283 103.964 191.283C89.3867 191.283 77.4673 179.16 77.4673 164.079C77.4673 148.997 89.3867 136.875 103.964 136.875C118.541 136.875 130.461 148.997 130.461 164.079Z" stroke="black" stroke-width="5"/>
<path d="M83.611 123.806L77.7959 154.178L88.263 141.354L103.964 135.955L119.083 141.354L130.131 152.153L104.545 21.2173L83.611 123.806Z" fill="black" stroke="black"/>
<path d="M206.842 107.5C206.842 165.553 161.036 212.5 104.671 212.5C48.306 212.5 2.5 165.553 2.5 107.5C2.5 49.4468 48.306 2.5 104.671 2.5C161.036 2.5 206.842 49.4468 206.842 107.5Z" stroke="black" stroke-width="5"/>
<rect x="23" y="14" width="40" height="60" fill="green" />
<ellipse cx="200" cy="0" rx="25" ry="35" fill="lightblue" />
<circle cx="300" cy="0" r="25" stroke="orange" stroke-width="4" />
<line x1="170" y1="75" x2="260" y2="105" stroke="blue" stroke-width="3" />
<polyline points="250 150 280 160 250 170 280 180" stroke="green" stroke-width="3" />
<polygon points="350,150 380,160 350,170 380,180" stroke="green" fill="lightgreen" />
<rect x="350" y="0" width="20" height="20" stroke="black" stroke-width="1" />
<rect x="380" y="0" width="20" height="20" stroke="black" stroke-width="6" />
<rect x="410" y="0" width="20" rx="10" height="20" stroke="black" stroke-width="1" />
<rect x="440" y="0" width="20" rx="10" height="20" stroke="black" stroke-width="6" />
<rect x="280" y="50" width="120" height="80" rx="40" ry="20" stroke="blue" stroke-width="3" fill="transparent" />
</svg>`)
document.getElementById("mySaveButton").addEventListener("click", save);
document.getElementById("myLoadButton").addEventListener("click", () => {
const svgstr = document.getElementById("mySavedSvg").value;
if (svgstr) load(svgstr);
});
</script>
</body>
</html>