export function toSVG()

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('');
    }