Sketch Plugin Development: Export Symbol as SVG Text - sketch-3

I have a working example of exporting a selected symbol to an SVG string in Sketch 3. (Based on this code from Sketch's GitHub)
The issue is the output is distorted and I'm unable to see the next logical step in troubleshooting.
The Code
// Get all symbols
var symbols = context.document.documentData().allSymbols();
// For purpose of this example, only get the first one for testing.
var symbol = symbols[0];
// Set a temporary file to save to. Necessary for generating the SVG
var tempPath = '/tmp/com.symbolui.sketch-commands/';
var guid = [[NSProcessInfo processInfo] globallyUniqueString];
var path = tempPath + guid;
[[NSFileManager defaultManager] createDirectoryAtPath:path withIntermediateDirectories:true attributes:nil error:nil]
var export_path = path;
var export_filename = export_path + '/' + 'test.svg';
// do export
[doc saveArtboardOrSlice:frame toFile:export_filename];
var file_url = [NSURL fileURLWithPath:export_filename];
var str = [[NSString alloc] initWithContentsOfURL:file_url];
At the final point in the code, str is a text value of the SVG generated from the symbol.
The Output
Input Symbol:
Generated preview of SVG:
Generated SVG text:
<?xml version="1.0" encoding="UTF-8"?>
<svg width="1200px" height="65px" viewBox="0 0 1200 65" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 42 (36781) - http://www.bohemiancoding.com/sketch -->
<title></title>
<desc>Created with Sketch.</desc>
<defs>
<rect id="path-1" x="0" y="0" width="49" height="20" rx="3"></rect>
<mask id="mask-2" maskContentUnits="userSpaceOnUse" maskUnits="objectBoundingBox" x="0" y="0" width="49" height="20" fill="white">
<use xlink:href="#path-1"></use>
</mask>
</defs>
<g id="Symbols:-Labels" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Buttons" transform="translate(-422.000000, 31.000000)"></g>
<g id="Label-/-Default" transform="translate(670.000000, 60.000000)">
<use id="background" stroke="#979797" mask="url(#mask-2)" stroke-width="2" fill="#777777" xlink:href="#path-1"></use>
<text id="-Default" font-family="HelveticaNeue-Bold, Helvetica Neue" font-size="10" font-weight="bold" fill="#FFFFFF">
<tspan x="7" y="14">Default</tspan>
</text>
</g>
</g>
Thoughts
As you can see the output is quite large in addition to being distorted. At the very least I believe I need to trim or rescale the symbol somehow. Where the distortion is coming through I have no idea.
Fixing the SVG markup itself isn't a solution - I'd like to see the solution in the Sketch plugin code. I'm finding documentation for internal Sketch code very difficult to work with and has been the big blocker for resolving this.

you can convert the MSSymbolInstance to normal group, then export it.
if (layer.symbolMaster().children().count() > 1) {
var tempSymbol = layer.duplicate(),
tempGroup = tempSymbol.detachByReplacingWithGroup();
tempGroup.resizeToFitChildrenWithOption(0);
layer.setIsVisible(false);
tempGroup.exportOptions().removeAllExportFormats();
var format = tempGroup.exportOptions().addExportFormat();
format.setFileFormat("SVG");
var slice = MSExportRequest.exportRequestsFromExportableLayer(tempGroup).firstObject();
var filePath = export_path + '/test.svg';
doc.saveArtboardOrSlice_toFile(slice, filePath);
}

Related

ExtrudeGeometry issue for SVG Containing Hole

I am currently looking for extruding the SVG below:
But I get the following result:
I would extrude only the two hexagon wall space. How can I do it ?
I tried both SVGLoader.createShapes(path); and //path.toShapes(true);
Here is my code:
const loader = new SVGLoader();
// load a SVG resource
loader.load(
// resource URL
'./svg/hexagone.svg',
// called when the resource is loaded
function (data) {
const paths = data.paths;
const svgGroup = new THREE.Group();
svgGroup.name = "svgGroup"
svgGroup.scale.y *= -1;
let mesh;
for (let i = 0; i < paths.length; i++) {
const path = paths[i];
const material = new THREE.MeshNormalMaterial();
const shapes = SVGLoader.createShapes(path); //path.toShapes(true);
shapes.forEach((shape, i) => {
const geometry = new THREE.ExtrudeGeometry(shape, {
depth: 10,
bevelEnabled: false
});
geometry.computeVertexNormals();
mesh = new THREE.Mesh(geometry, material);
svgGroup.add(mesh);
});
}
// Reshape items
const box = new THREE.Box3().setFromObject(svgGroup);
const size = new THREE.Vector3();
box.getSize(size);
const zOffset = size.z / -2;
const yOffset = size.y / -2;
const xOffset = size.x / -2;
svgGroup.children.forEach(item => {
item.position.z = zOffset;
item.position.x = xOffset;
item.position.y = yOffset;
});
scene.add(new THREE.AxesHelper(5))
scene.add(svgGroup)
// Rotate
svgGroup.rotateX(90 * Math.PI / 180);
}
My add my SVG below. When I tried to debug, I get 4 meshes. 1 for each line, 1 for the outside hexagon (overlapping the inside hexagon), 1 for the inside hexagon.
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 53.86 53.86">
<defs>
<style>
.cls-1 {
fill: #fff;
}
</style>
</defs>
<g>
<polygon class="cls-1" points="15.98 53.36 .5 37.88 .5 15.98 15.98 .5 37.88 .5 53.36 15.98 53.36 37.88 37.88 53.36 15.98 53.36"/>
<path d="M37.67,1l15.19,15.19v21.48l-15.19,15.19H16.19L1,37.67V16.19L16.19,1h21.48m.41-1H15.77L0,15.77v22.31l15.77,15.77h22.31l15.77-15.77V15.77L38.08,0h0Z"/>
</g>
<g>
<polygon class="cls-1" points="20.63 44.31 10.96 34.64 10.96 20.96 20.63 11.29 34.31 11.29 43.98 20.96 43.98 34.64 34.31 44.31 20.63 44.31"/>
<path d="M34.1,11.79l9.38,9.38v13.26l-9.38,9.38h-13.26l-9.38-9.38v-13.26l9.38-9.38h13.26m.41-1h-14.09l-9.96,9.96v14.09l9.96,9.96h14.09l9.96-9.96v-14.09l-9.96-9.96h0Z"/>
</g>
</svg>
With another SVG (see below) the result is the same even if in this scenario I have 2 paths in the SVG.
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Fusion 360, Shaper Origin Export Add-In, Version 1.6.10 -->
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xmlns:shaper="http://www.shapertools.com/namespaces/shaper" shaper:fusionaddin="version:1.6.10" width="3.1176915cm" height="3.6cm" version="1.1" x="0cm" y="0cm" viewBox="0 0 3.1176915 3.6" enable-background="new 0 0 3.1176915 3.6" xml:space="preserve">
<path d="M-0,-1.8 L1.5588457,-0.9 1.5588457,0.9 -0,1.8 -1.5588457,0.9 -1.5588457,-0.9 -0,-1.8z" transform="matrix(1,0,0,-1,1.5588457,1.8)" fill="rgb(0,0,0)" stroke-linecap="round" stroke-linejoin="round" />
<path d="M-0.9588457,0.5535898 L-0.9588457,-0.5535898 -0,-1.1071797 0.9588457,-0.5535898 0.9588457,0.5535898 0,1.1071797 -0.9588457,0.5535898z" transform="matrix(1,0,0,-1,1.5588457,1.8)" fill="rgb(255,255,255)" stroke="rgb(0,0,0)" stroke-width="0.0010cm" stroke-linecap="round" stroke-linejoin="round" />
</svg>
Hexagon inside is also recovered by the biggest.
I finally chose to use lnkscape to generate my SVG. I have mush more options and I was able to generate the hole by combining all pathes in 1 path.

azure: server-side SVG to PNG in node.js azure function app? nothing works

does anyone know how to convert SVG images to PNG (or JPG) on the server side? i've a node.js script that needs to do this and send the PNG to the caller. no rendering can be done on the client, it can only read PNGs or JPGs. i've looked at phantomjs, anychart-nodejs, canvas and others. they simply don't install or in the case of phantomjs, it installs and works in some cases and not in others. phantomjs works perfectly in node.js on a windows 10 computer, btw.
any ideas? thanks!
i figured it out. using the sharp js library did the trick. it's a bit difficult to get going, though. send me a message if you have trouble; i'll post my solution here.
My idea is to render a SVG image to canvas HTML element and then to convert the canvas one to a png image.
Here is my sample code demo.html as below, which I am using a pure JavaScript package canvg/canvg in browser.
<html>
<head>
<script type="text/javascript" src="https://unpkg.com/canvg#3.0.4/lib/umd.js"></script>
</head>
<body>
<svg id="svg_logo" xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
height="300" width="300" viewBox="0 0 300 300">
<defs>
<g id="SVG" fill="#ffffff" transform="scale(2) translate(20,79)">
<path id="S" d="M 5.482,31.319 C2.163,28.001 0.109,23.419 0.109,18.358 C0.109,8.232 8.322,0.024 18.443,0.024 C28.569,0.024 36.782,8.232 36.782,18.358 L26.042,18.358 C26.042,14.164 22.638,10.765 18.443,10.765 C14.249,10.765 10.850,14.164 10.850,18.358 C10.850,20.453 11.701,22.351 13.070,23.721 L13.075,23.721 C14.450,25.101 15.595,25.500 18.443,25.952 L18.443,25.952 C23.509,26.479 28.091,28.006 31.409,31.324 L31.409,31.324 C34.728,34.643 36.782,39.225 36.782,44.286 C36.782,54.412 28.569,62.625 18.443,62.625 C8.322,62.625 0.109,54.412 0.109,44.286 L10.850,44.286 C10.850,48.480 14.249,51.884 18.443,51.884 C22.638,51.884 26.042,48.480 26.042,44.286 C26.042,42.191 25.191,40.298 23.821,38.923 L23.816,38.923 C22.441,37.548 20.468,37.074 18.443,36.697 L18.443,36.692 C13.533,35.939 8.800,34.638 5.482,31.319 L5.482,31.319 L5.482,31.319 Z"/>
<path id="V" d="M 73.452,0.024 L60.482,62.625 L49.742,62.625 L36.782,0.024 L47.522,0.024 L55.122,36.687 L62.712,0.024 L73.452,0.024 Z"/>
<path id="G" d="M 91.792,25.952 L110.126,25.952 L110.126,44.286 L110.131,44.286 C110.131,54.413 101.918,62.626 91.792,62.626 C81.665,62.626 73.458,54.413 73.458,44.286 L73.458,44.286 L73.458,18.359 L73.453,18.359 C73.453,8.233 81.665,0.025 91.792,0.025 C101.913,0.025 110.126,8.233 110.126,18.359 L99.385,18.359 C99.385,14.169 95.981,10.765 91.792,10.765 C87.597,10.765 84.198,14.169 84.198,18.359 L84.198,44.286 L84.198,44.286 C84.198,48.481 87.597,51.880 91.792,51.880 C95.981,51.880 99.380,48.481 99.385,44.291 L99.385,44.286 L99.385,36.698 L91.792,36.698 L91.792,25.952 L91.792,25.952 Z"/>
</g>
</defs>
<path id="base" fill="#000" d="M8.5,150 H291.5 V250 C291.5,273.5 273.5,291.5 250,291.5 H50 C26.5,291.5 8.5,273.5 8.5,250 Z"/>
<g stroke-width="38.0086" stroke="#000">
<g id="svgstar" transform="translate(150, 150)">
<path id="svgbar" fill="#ffb13b"
d="M-84.1487,-15.8513 a22.4171,22.4171 0 1 0 0,31.7026 h168.2974 a22.4171,22.4171 0 1 0 0,-31.7026 Z"/>
<use xlink:href="#svgbar" transform="rotate(45)"/>
<use xlink:href="#svgbar" transform="rotate(90)"/>
<use xlink:href="#svgbar" transform="rotate(135)"/>
</g>
</g>
<use xlink:href="#svgstar"/>
<use xlink:href="#base" opacity="0.85"/>
<use xlink:href="#SVG"/>
</svg>
<script type="text/javascript">
var svg_content=document.getElementById('svg_logo').outerHTML;
window.onload = () => {
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
v = canvg.Canvg.fromString(ctx, svg_content);//'<svg width="600" height="600"><text x="50" y="50">Hello World!</text></svg>');
// Start SVG rendering with animations and mouse handling.
v.start();
var MIME_TYPE = "image/png";
var imgURL = canvas.toDataURL(MIME_TYPE);
var dlLink = document.createElement('a');
dlLink.download = fileName;
dlLink.href = imgURL;
dlLink.dataset.downloadurl = [MIME_TYPE, dlLink.download, dlLink.href].join(':');
document.body.appendChild(dlLink);
dlLink.click();
document.body.removeChild(dlLink);
};
</script>
<canvas />
</body>
</html>
var fs = require('fs');
var svg2img = require('svg2img');
var svgString = [
'<svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="236" height="120" ',
'viewBox="0 0 236 120">',
'<rect x="14" y="23" width="200" height="50" fill="#55FF55" stroke="black" stroke-width="1" />',
'</svg>'
].join('');
//convert from svg string
svg2img(svgString, function(error, buffer) {
//returns a Buffer
fs.writeFileSync('foo1.png', buffer);
});
You will find more info here:-
https://www.npmjs.com/package/svg2img

Vue can not attach event listener

I have an svg with some elements in it, the complete code is in here capture event, the aircraft image is positioned via transform attribute in such a way that it falls into the image with href2. The problem is Vue is unable to detect the click event on the aircraft image.
I can't seem to find a way to go around this. I want to be able to attach an event listener to the aircraft image regardless of where on the screen is located.
In jQuery solving these kind of situations is like a breeze of air, but with Vue seems to be a different story.
Here is the HTML
<div id="app">
<svg xmlns="http://www.w3.org/2000/svg" width="1015px" height="580px" viewBox="-50 -50 1015 580" preserveAspectRatio="xMidYMid meet" version="1.1" id="svg">
<g #click="showFlightCard" v-for="(ge, index) in this.gEl" :key="index">
<path :id="index+1" d="M 400 100 L 150 150" stroke="red" stroke-width="3" fill="none" />
<image :id="index" :href="href1" width="48" height="24" transform="translate(251,143)"></image>
</g>
<image x="250" y="10" width="522" height="402.452" id="e4_image" preserveAspectRatio="xMidYMid meet" :href="href2" style="stroke:black;stroke-width:1px;fill:khaki;"/>
</svg>
</div>
Here is the JS, for clarity I avoided the href in here due to 64 bit encoding, which is too long, please look at the jsfiddle which contains the href as well.
var app = new Vue({
el: '#app',
methods: {
showFlightCard: function (e) {
console.log('Click')
}
},
data: {
message: 'Hello Vue!',
gEl: ['A', 'B', 'C'],
href1: 'look at the jsfiddle',
href2: 'look at the jsfiddle'
}
})
You can attach the click listener on the path of the svg itself, or on the parent element of the svg. A click listener on the svg tag itself doesn't seem to fire events.

How can i make a svg <rect> box dynamic with changeable <text>?

How can i make a svg rect box dynamic with changeable text ? Like in my code, if the text "Hello" will more than 30 character ?
<svg version="1.1" viewBox="0 0 500 500" preserveAspectRatio="xMinYMin meet" class="svg-content">
<g>
<rect x="0" y="0" width="100" height="100" stroke-width="5" stroke="#000000" fill="none"></rect> ?
<text x="0" y="50" font-family="Verdana" font-size="35" fill="blue">Hello</text>
</g>
</svg>
Probably the best approach would be to have the rect preset at specific width. Then create tspans to fill the text element, and dynamically resize the rect height as the characters exceed the preset width.
Below is an example:
<!DOCTYPE HTML>
<html>
<head>
<title>Wrap Text Rectangle</title>
</head>
<body onload=wrapTextRect()>
Place this text:<br>
<textarea id="myTextValue" style='width:400px;height:60px;'>
Hello!
</textarea><br>
<button onClick=wrapTextRect()> Wrap text in rect</button>
<div id="svgDiv" style='background-color:lightgreen;width:400px;height:400px;'>
<svg id="mySVG" width="400" height="400">
<rect id=myRect x=50 y=50 rx=10 ry=10 width=100 fill="#4682b4" stroke='black' stroke-width=5 opacity=.5 />
<text id=myText font-size=14 font-family="arial" fill="white" />
</svg>
</div>
SVG Source:<br>
<textarea id=sourceValue style=width:500px;height:300px></textarea>
<script>
var NS="http://www.w3.org/2000/svg"
//---onload and button---
function wrapTextRect()
{
//---clear previous---
for(var k=myText.childNodes.length-1;k>=0;k--)
myText.removeChild(myText.childNodes.item(k))
var padding=10
var width=+myRect.getAttribute("width")-padding
var x=+myRect.getAttribute("x")
var y=+myRect.getAttribute("y")
var fontSize=+myText.getAttribute("font-size")
var text=myTextValue.value
var words = text.split(' ');
var text_element = document.getElementById('myText');
var tspan_element = document.createElementNS(NS, "tspan"); // Create first tspan element
var text_node = document.createTextNode(words[0]); // Create text in tspan element
tspan_element.setAttribute("x", x+padding);
tspan_element.setAttribute("y", y+padding+fontSize);
tspan_element.appendChild(text_node); // Add tspan element to DOM
text_element.appendChild(tspan_element); // Add text to tspan element
//---[EDIT] a single word that exceeds preset rect with---
if(words.length==1)
{
var len = tspan_element.getComputedTextLength()
if(len>+myRect.getAttribute("width"))
myRect.setAttribute("width", len+2*padding)
}
//---end [EDIT]------------------
for(var i=1; i<words.length; i++)
{
var len = tspan_element.firstChild.data.length // Find number of letters in string
tspan_element.firstChild.data += " " + words[i]; // Add next word
if (tspan_element.getComputedTextLength() > width-padding)
{
tspan_element.firstChild.data = tspan_element.firstChild.data.slice(0, len); // Remove added word
var tspan_element = document.createElementNS(NS, "tspan"); // Create new tspan element
tspan_element.setAttribute("x", x+padding);
tspan_element.setAttribute("dy", fontSize);
text_node = document.createTextNode(words[i]);
tspan_element.appendChild(text_node);
text_element.appendChild(tspan_element);
}
}
var height = text_element.getBBox().height +2*padding; //-- get height plus padding
myRect.setAttribute('height', height); //-- change rect height
//---show svg source---
sourceValue.value=svgDiv.innerHTML
}
</script>
</body>
</html>

Zoom on multiple areas in d3.js

I'm planning to have a geoJSON map inside my svg alongside other svg elements. I would like to be able to zoom (zoom+pan) in the map and keep the map in the same location with a bounding box. I can accomplish this by using a clipPath to keep the map within a rectangular area. The problem is that I also want to enable zooming and panning on my entire svg. If I do d3.select("svg").call(myzoom); this overrides any zoom I applied to my map. How can I apply zoom to both my entire svg and to my map? That is, I want to be able to zoom+pan on my map when my mouse is in the map's bounding box, and when the mouse is outside the bounding box, zoom+pan on the entire svg.
Here's example code: http://bl.ocks.org/nuernber/aeaac0e8edcf7ca93ade.
<svg id="svg" width="640" height="480" xmlns="http://www.w3.org/2000/svg" version="1.1">
<defs>
<clipPath id="rectClip">
<rect x="150" y="25" width="400" height="400" style="stroke: gray; fill: none;"/>
</clipPath>
</defs>
<g id="outer_group">
<circle cx="100" cy="50" r="40" stroke="black" stroke-width="2" fill="red" />
<g id="svg_map" style="clip-path: url(#rectClip);">
</g>
</g>
</svg><br/>
<script type="text/javascript">
var svg = d3.select("#svg_map");
var mapGroup = svg.append("g");
var projection = d3.geo.mercator();
var path = d3.geo.path().projection(projection);
var zoom = d3.behavior.zoom()
.translate(projection.translate())
.scale(projection.scale())
.on("zoom", zoomed);
mapGroup.call(zoom);
var pan = d3.behavior.zoom()
.on("zoom", panned);
d3.select("svg").call(pan);
mapGroup.attr("transform", "translate(200,0) scale(2,2)");
d3.json("ne_110m_admin_0_countries/ne_110m_admin_0_countries.geojson", function(collection) {
mapGroup.selectAll("path").data(collection.features)
.enter().append("path")
.attr("d", path)
.attr("id", function(d) { return d.properties.name.replace(/\s+/g, "")})
.style("fill", "gray").style("stroke", "white").style("stroke-width",1);
}
);
function panned() {
var x = d3.event.translate[0];
var y = d3.event.translate[1];
d3.select("#outer_group").attr("transform", "translate("+x+","+y+") scale(" + d3.event.scale + ")");
}
function zoomed() {
previousScale = d3.event.scale;
projection.translate(d3.event.translate).scale(d3.event.scale);
translationOffset = d3.event.translate;
mapGroup.selectAll("path").attr("d", path);
}
</script>
You need two zoom behaviours for that. The first one would be attached to the SVG and the second one to the map. In the zoom handlers you would have to take care of taking the appropriate action for each.

Resources