in packages/maker.js/src/core/svg.ts [318:742]
export function toSVG(itemToExport: any, options?: ISVGRenderOptions): string {
function append(value: string, layer?: string, forcePush = false) {
if (!forcePush && typeof layer == "string" && layer.length > 0) {
if (!(layer in layers)) {
layers[layer] = [];
}
layers[layer].push(value);
} else {
elements.push(value);
}
}
function cssStyle(elOpts: ISVGElementRenderOptions) {
var a: string[] = [];
function push(name: string, val: string) {
if (val === undefined) return;
a.push(name + ':' + val);
}
push('stroke', elOpts.stroke);
push('stroke-width', elOpts.strokeWidth);
push('fill', elOpts.fill);
return a.join(';');
}
function addSvgAttrs(attrs: IXmlTagAttrs, elOpts: ISVGElementRenderOptions) {
if (!elOpts) return;
extendObject(attrs, {
"stroke": elOpts.stroke,
"stroke-width": elOpts.strokeWidth,
"fill": elOpts.fill,
"style": elOpts.cssStyle || cssStyle(elOpts)
});
}
function colorLayerOptions(layer: string): ISVGElementRenderOptions {
if (opts.layerOptions && opts.layerOptions[layer]) return opts.layerOptions[layer];
if (layer in colors) {
return {
stroke: layer
};
}
}
function createElement(tagname: string, attrs: IXmlTagAttrs, layer: string, innerText: string = null, forcePush = false) {
if (tagname !== 'text') {
addSvgAttrs(attrs, colorLayerOptions(layer));
}
if (!opts.scalingStroke) {
attrs['vector-effect'] = 'non-scaling-stroke';
}
var tag = new XmlTag(tagname, attrs);
tag.closingTags = opts.closingTags;
if (innerText) {
tag.innerText = innerText;
}
append(tag.toString(), layer, forcePush);
}
function fixPoint(pointToFix: IPoint): IPoint {
//in DXF Y increases upward. in SVG, Y increases downward
var pointMirroredY = svgCoords(pointToFix);
return point.scale(pointMirroredY, opts.scale);
}
function fixPath(pathToFix: IPath, origin: IPoint): IPath {
//mirror creates a copy, so we don't modify the original
var mirrorY = path.mirror(pathToFix, false, true);
return path.moveRelative(path.scale(mirrorY, opts.scale), origin);
}
//fixup options
var opts: ISVGRenderOptions = {
accuracy: .001,
annotate: false,
origin: null,
scale: 1,
stroke: "#000",
strokeLineCap: "round",
strokeWidth: '0.25mm', //a somewhat average kerf of a laser cutter
fill: "none",
fillRule: "evenodd",
fontSize: '9pt',
useSvgPathOnly: true,
viewBox: true
};
extendObject(opts, options);
var modelToExport: IModel;
var itemToExportIsModel = isModel(itemToExport);
if (itemToExportIsModel) {
modelToExport = itemToExport as IModel;
if (modelToExport.exporterOptions) {
extendObject(opts, modelToExport.exporterOptions['toSVG']);
}
}
var elements: string[] = [];
var layers: { [id: string]: string[]; } = {};
//measure the item to move it into svg area
if (itemToExportIsModel) {
modelToExport = <IModel>itemToExport;
} else if (Array.isArray(itemToExport)) {
//issue: this won't handle an array of models
var pathMap: IPathMap = {};
(itemToExport as IPath[]).forEach((p, i) => { pathMap[i] = p });
modelToExport = { paths: pathMap };
} else if (isPath(itemToExport)) {
modelToExport = { paths: { modelToMeasure: <IPath>itemToExport } };
}
const size = measure.modelExtents(modelToExport);
//increase size to fit caption text
const captions = model.getAllCaptionsOffset(modelToExport);
captions.forEach(caption => {
measure.increase(size, measure.pathExtents(caption.anchor), true);
});
//try to get the unit system from the itemToExport
if (!opts.units) {
var unitSystem = tryGetModelUnits(itemToExport);
if (unitSystem) {
opts.units = unitSystem;
}
}
//convert unit system (if it exists) into SVG's units. scale if necessary.
var useSvgUnit = svgUnit[opts.units];
if (useSvgUnit && opts.viewBox) {
opts.scale *= useSvgUnit.scaleConversion;
}
if (size && !opts.origin) {
var left = -size.low[0] * opts.scale;
opts.origin = [left, size.high[1] * opts.scale];
}
//also pass back to options parameter
extendObject(options, opts);
//begin svg output
var svgAttrs: IXmlTagAttrs = {};
if (size && opts.viewBox) {
var width = round(size.width * opts.scale, opts.accuracy);
var height = round(size.height * opts.scale, opts.accuracy);
var viewBox = [0, 0, width, height];
var unit = useSvgUnit ? useSvgUnit.svgUnitType : '';
svgAttrs = {
width: width + unit,
height: height + unit,
viewBox: viewBox.join(' ')
};
}
svgAttrs["xmlns"] = "http://www.w3.org/2000/svg";
var svgTag = new XmlTag('svg', <IXmlTagAttrs>extendObject(svgAttrs, opts.svgAttrs));
append(svgTag.getOpeningTag(false));
var groupAttrs: IXmlTagAttrs = {
id: 'svgGroup',
"stroke-linecap": opts.strokeLineCap,
"fill-rule": opts.fillRule,
"font-size": opts.fontSize
};
addSvgAttrs(groupAttrs, opts);
var svgGroup = new XmlTag('g', groupAttrs);
append(svgGroup.getOpeningTag(false));
if (opts.useSvgPathOnly) {
var findChainsOptions: IFindChainsOptions = {
byLayers: true
};
if (opts.fillRule === 'nonzero') {
findChainsOptions.contain = <IContainChainsOptions>{
alternateDirection: true
}
}
var pathDataByLayer = getPathDataByLayer(modelToExport, opts.origin, findChainsOptions, opts.accuracy);
for (var layerId1 in pathDataByLayer) {
var pathData = pathDataByLayer[layerId1].join(' ');
var attrs = { "d": pathData };
if (layerId1.length > 0) {
attrs["id"] = layerId1;
}
createElement("path", attrs, layerId1, null, true);
}
} else {
function drawText(id: string, textPoint: IPoint, layer: string) {
createElement(
"text",
{
"id": id + "_text",
"x": round(textPoint[0], opts.accuracy),
"y": round(textPoint[1], opts.accuracy)
},
layer,
id);
}
function drawPath(id: string, x: number, y: number, d: ISvgPathData, layer: string, route: string[], textPoint: IPoint, annotate: boolean, flow: IFlowAnnotation) {
createElement(
"path",
{
"id": id,
"data-route": route,
"d": ["M", round(x, opts.accuracy), round(y, opts.accuracy)].concat(d).join(" ")
},
layer);
if (annotate) {
drawText(id, textPoint, layer);
}
}
function circleInPaths(id: string, center: IPoint, radius: number, layer: string, route: string[], annotate: boolean, flow: IFlowAnnotation) {
var d = svgCircleData(radius, opts.accuracy);
drawPath(id, center[0], center[1], d, layer, route, center, annotate, flow);
}
var map: { [type: string]: (id: string, pathValue: IPath, layer: string, className: string, route: string[], annotate: boolean, flow: IFlowAnnotation) => void; } = {};
map[pathType.Line] = function (id: string, line: IPathLine, layer: string, className: string, route: string[], annotate: boolean, flow: IFlowAnnotation) {
var start = line.origin;
var end = line.end;
createElement(
"line",
{
"id": id,
"class": className,
"data-route": route,
"x1": round(start[0], opts.accuracy),
"y1": round(start[1], opts.accuracy),
"x2": round(end[0], opts.accuracy),
"y2": round(end[1], opts.accuracy)
},
layer);
if (annotate) {
drawText(id, point.middle(line), layer);
}
if (flow) {
addFlowMarks(flow, layer, line.origin, line.end, angle.ofLineInDegrees(line));
}
};
map[pathType.Circle] = function (id: string, circle: IPathCircle, layer: string, className: string, route: string[], annotate: boolean, flow: IFlowAnnotation) {
var center = circle.origin;
createElement(
"circle",
{
"id": id,
"class": className,
"data-route": route,
"r": circle.radius,
"cx": round(center[0], opts.accuracy),
"cy": round(center[1], opts.accuracy)
},
layer);
if (annotate) {
drawText(id, center, layer);
}
};
map[pathType.Arc] = function (id: string, arc: IPathArc, layer: string, className: string, route: string[], annotate: boolean, flow: IFlowAnnotation) {
correctArc(arc);
var arcPoints = point.fromArc(arc);
if (measure.isPointEqual(arcPoints[0], arcPoints[1])) {
circleInPaths(id, arc.origin, arc.radius, layer, route, annotate, flow);
} else {
var d = ['A'];
svgArcData(
d,
arc.radius,
arcPoints[1],
opts.accuracy,
angle.ofArcSpan(arc) > 180,
arc.startAngle > arc.endAngle
);
drawPath(id, arcPoints[0][0], arcPoints[0][1], d, layer, route, point.middle(arc), annotate, flow);
if (flow) {
addFlowMarks(flow, layer, arcPoints[1], arcPoints[0], angle.noRevolutions(arc.startAngle - 90));
}
}
};
map[pathType.BezierSeed] = function (id: string, seed: IPathBezierSeed, layer: string, className: string, route: string[], annotate: boolean, flow: IFlowAnnotation) {
var d: ISvgPathData = [];
svgBezierData(d, seed, opts.accuracy);
drawPath(id, seed.origin[0], seed.origin[1], d, layer, route, point.middle(seed), annotate, flow);
};
function addFlowMarks(flow: IFlowAnnotation, layer: string, origin: IPoint, end: IPoint, endAngle: number) {
const className = 'flow';
//origin: add a circle
map[pathType.Circle]('', new paths.Circle(origin, flow.size / 2), layer, className, null, false, null);
//end: add an arrow
const arrowEnd: IPoint = [-1 * flow.size, flow.size / 2];
const arrowLines = [arrowEnd, point.mirror(arrowEnd, false, true)].map(p => new paths.Line(point.add(point.rotate(p, endAngle), end), end));
arrowLines.forEach(a => map[pathType.Line]('', a, layer, className, null, false, null));
}
function beginModel(id: string, modelContext: IModel) {
modelGroup.attrs = { id: id };
append(modelGroup.getOpeningTag(false), modelContext.layer);
}
function endModel(modelContext: IModel) {
append(modelGroup.getClosingTag(), modelContext.layer);
}
var modelGroup = new XmlTag('g');
var walkOptions: IWalkOptions = {
beforeChildWalk: (walkedModel: IWalkModel): boolean => {
beginModel(walkedModel.childId, walkedModel.childModel);
return true;
},
onPath: (walkedPath: IWalkPath) => {
var fn = map[walkedPath.pathContext.type];
if (fn) {
var offset = point.add(fixPoint(walkedPath.offset), opts.origin);
fn(walkedPath.pathId, fixPath(walkedPath.pathContext, offset), walkedPath.layer, null, walkedPath.route, opts.annotate, opts.flow);
}
},
afterChildWalk: (walkedModel: IWalkModel) => {
endModel(walkedModel.childModel);
}
};
beginModel('0', modelToExport);
model.walk(modelToExport, walkOptions);
//export layers as groups
for (var layerId2 in layers) {
var layerGroup = new XmlTag('g', { id: layerId2 });
addSvgAttrs(layerGroup.attrs, colorLayerOptions(layerId2));
for (var i = 0; i < layers[layerId2].length; i++) {
layerGroup.innerText += layers[layerId2][i];
}
layerGroup.innerTextEscaped = true;
append(layerGroup.toString());
}
endModel(modelToExport);
}
const captionTags = captions.map(caption => {
const anchor = fixPath(caption.anchor, opts.origin) as IPathLine;
const center = point.rounded(point.middle(anchor), opts.accuracy);
const tag = new XmlTag('text', {
"alignment-baseline": "middle",
"text-anchor": "middle",
"transform": `rotate(${angle.ofLineInDegrees(anchor)},${center[0]},${center[1]})`,
"x": center[0],
"y": center[1]
});
addSvgAttrs(tag.attrs, colorLayerOptions(caption.layer));
tag.innerText = caption.text;
return tag.toString();
});
if (captionTags.length) {
const captionGroup = new XmlTag('g', { "id": "captions" });
addSvgAttrs(captionGroup.attrs, colorLayerOptions(captionGroup.attrs.id));
captionGroup.innerText = captionTags.join('');
captionGroup.innerTextEscaped = true;
append(captionGroup.toString());
}
append(svgGroup.getClosingTag());
append(svgTag.getClosingTag());
return elements.join('');
}