From 98648c494b03fb0716002147fb841b5f7f66b351 Mon Sep 17 00:00:00 2001 From: Toby Schachman Date: Wed, 2 Mar 2016 17:00:45 -0800 Subject: [PATCH 1/6] Adding SVG export support. This adds support for groups, paths, and partial support for circles. --- src/Graphic/Graphic.coffee | 77 ++++++++++++++++++++++++++++++++++++-- src/Model/Editor.coffee | 19 ++++++++++ 2 files changed, 92 insertions(+), 4 deletions(-) diff --git a/src/Graphic/Graphic.coffee b/src/Graphic/Graphic.coffee index d8dc525..c7af7e7 100644 --- a/src/Graphic/Graphic.coffee +++ b/src/Graphic/Graphic.coffee @@ -60,6 +60,19 @@ class Graphic.Element ### throw "Not implemented" + toSvg: (opts) -> + ### + + Returns an svg for the graphic as a string. The svg string does not have a + wrapper element. + + Opts: + + viewMatrix: + + ### + throw "Not implemented" + # =========================================================================== # Helpers @@ -91,6 +104,11 @@ class Graphic.Group extends Graphic.Element else return null + toSvg: (opts) -> + svgString = "" + for childGraphic in @childGraphics + svgString += childGraphic.toSvg(opts) + return svgString class Graphic.Anchor extends Graphic.Element @@ -117,10 +135,29 @@ class Graphic.Path extends Graphic.Element else return null + toSvg: ({viewMatrix}) -> + anchors = @collectAnchors() + pointStrings = [] + for anchor in anchors + [x, y] = viewMatrix.compose(anchor.matrix).origin() + pointStrings.push("#{x},#{y}") + pointsAttribute = "points=\"#{pointStrings.join(" ")}\"" + paintAttributes = @svgPaintAttributes() + elementName = if @isClosed() then "polygon" else "polyline" + return "<#{elementName} #{pointsAttribute} #{paintAttributes} />" + + performPaintOps: ({ctx}) -> for component in @componentsOfType(Graphic.PaintOp) component.paint(ctx) + svgPaintAttributes: -> + # Note: below needs to be fixed once Elements are allowed to have multiple + # fill, stroke, etc. Components. + fillAttribute = @componentOfType(Graphic.Fill).toSvg() + strokeAttribute = @componentOfType(Graphic.Stroke).toSvg() + return "#{fillAttribute} #{strokeAttribute}" + highlightIfNecessary: ({highlight, ctx}) -> return unless highlight highlightSpec = highlight(this) @@ -168,6 +205,21 @@ class Graphic.Circle extends Graphic.Path ctx.arc(0, 0, 1, 0, 2 * Math.PI, false) ctx.restore() + toSvg: ({viewMatrix}) -> + matrix = viewMatrix.compose(@matrix) + # TODO: In canvas we can transform a path without transforming a stroke. + # In SVG, the only way to do this is with the vector-effect attribute. But + # that is not supported in SVG 1.1, and I want this to work with CairoSVG, + # laser cutter, etc. So for now, I'm just going to assume all circles are + # transformed with only translation, rotation, and uniform scaling; not + # the more general case of all affine transformations. + r = Math.sqrt(matrix.a*matrix.a + matrix.b*matrix.b) + cx = matrix.e + cy = matrix.f + paintAttributes = @svgPaintAttributes() + return "" + + class Graphic.Text extends Graphic.Path render: (opts) -> @@ -180,6 +232,10 @@ class Graphic.Text extends Graphic.Path @buildPath(opts) @highlightIfNecessary(opts) + toSvg: ({viewMatrix}) -> + # TODO + return "" + textComponent: -> @componentOfType(Graphic.TextComponent) @@ -264,10 +320,20 @@ class Graphic.PaintOp extends Graphic.Component class Graphic.Fill extends Graphic.PaintOp paint: (ctx) -> - ctx.save() - ctx.fillStyle = @color - ctx.fill() - ctx.restore() + unless @isTransparent() + ctx.save() + ctx.fillStyle = @color + ctx.fill() + ctx.restore() + + toSvg: -> + if @isTransparent() + return "fill=\"none\"" + else + return "fill=\"#{@color}\"" + + isTransparent: -> + return @color == "transparent" or /^rgba\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*,\s*0\s*\)$/.test(@color) class Graphic.Stroke extends Graphic.PaintOp paint: (ctx) -> @@ -278,6 +344,9 @@ class Graphic.Stroke extends Graphic.PaintOp ctx.stroke() ctx.restore() + toSvg: -> + return "stroke=\"#{@color}\" stroke-width=\"#{@lineWidth}\"" + class Graphic.PathComponent extends Graphic.Component class Graphic.TextComponent extends Graphic.Component diff --git a/src/Model/Editor.coffee b/src/Model/Editor.coffee index 8b95cb6..d4cffc5 100644 --- a/src/Model/Editor.coffee +++ b/src/Model/Editor.coffee @@ -178,6 +178,25 @@ module.exports = class Editor .done() + # =========================================================================== + # Export + # =========================================================================== + + exportSvg: -> + # These scaling parameters are arbitrary. They should be configurable when + # you export. + viewMatrix = new Util.Matrix(100, 0, 0, -100, 0, 0) # scale, flip Y + opts = {viewMatrix} + graphics = @project.editingElement.allGraphics() + svgString = "" + for graphic in graphics + svgString += graphic.toSvg(opts) + svgString += "" + + fileName = @project.editingElement.label + ".svg" + Storage.saveFile(svgString, fileName, "image/svg+xml;charset=utf-8") + + # =========================================================================== # Revision History # =========================================================================== From d2bd04da9c924ac145b5c221f4629bdd8dedf22c Mon Sep 17 00:00:00 2001 From: Toby Schachman Date: Thu, 5 Jan 2017 13:20:19 -0800 Subject: [PATCH 2/6] Finished implementation for Graphic.Circle's toSvg method --- src/Graphic/Graphic.coffee | 43 ++++++++++++++++++++++++++++---------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/Graphic/Graphic.coffee b/src/Graphic/Graphic.coffee index f375b56..54491b8 100644 --- a/src/Graphic/Graphic.coffee +++ b/src/Graphic/Graphic.coffee @@ -212,19 +212,40 @@ class Graphic.Circle extends Graphic.Path ctx.restore() toSvg: ({viewMatrix}) -> - matrix = viewMatrix.compose(@matrix) - # TODO: In canvas we can transform a path without transforming a stroke. - # In SVG, the only way to do this is with the vector-effect attribute. But - # that is not supported in SVG 1.1, and I want this to work with CairoSVG, - # laser cutter, etc. So for now, I'm just going to assume all circles are - # transformed with only translation, rotation, and uniform scaling; not - # the more general case of all affine transformations. - r = Math.sqrt(matrix.a*matrix.a + matrix.b*matrix.b) - cx = matrix.e - cy = matrix.f paintAttributes = @svgPaintAttributes() - return "" + matrix = viewMatrix.compose(@matrix) + + # In canvas we can transform a path without transforming a stroke. In SVG, + # the only way to do this is with the vector-effect attribute. But that is + # not supported in SVG 1.1, and I want this to work with CairoSVG, laser + # cutter, etc. + # To deal with this, we'll use a element if we can (if the + # transform is just a uniform scaling) and otherwise we'll approximate the + # circle (really an ellipse at this point) with a bezier curve. + + {a, b, c, d, e, f} = matrix + r1 = Math.sqrt(a*a + b*b) + r2 = Math.sqrt(c*c + d*d) + if Math.abs(r1 - r2) < .000000001 + return "" + else + # Using http://spencermortensen.com/articles/bezier-circle/ + cp = 0.551915024494 + beziers = [ + [[1,cp], [cp,1], [0,1]] + [[-cp,1], [-1,cp], [-1,0]] + [[-1,-cp], [-cp, -1], [0,-1]] + [[cp,-1], [1,-cp], [1,0]] + ] + transformedFirstPoint = matrix.fromLocal([1,0]) + path = "M #{transformedFirstPoint[0]} #{transformedFirstPoint[1]}" + for bezier in beziers + path += " C" + for point in bezier + transformedPoint = matrix.fromLocal(point) + path += " #{transformedPoint[0]} #{transformedPoint[1]}" + return "" class Graphic.Text extends Graphic.Path From d4fac8ae18d4d6a765a70cda6dab8cfbeda905c0 Mon Sep 17 00:00:00 2001 From: Toby Schachman Date: Thu, 5 Jan 2017 14:37:21 -0800 Subject: [PATCH 3/6] Implemented Graphic.Text's toSvg method --- src/Graphic/Graphic.coffee | 19 +++++++++++++++++-- src/Util/Matrix.coffee | 4 ++-- src/Util/Util.coffee | 5 +++++ 3 files changed, 24 insertions(+), 4 deletions(-) diff --git a/src/Graphic/Graphic.coffee b/src/Graphic/Graphic.coffee index 54491b8..1174dbb 100644 --- a/src/Graphic/Graphic.coffee +++ b/src/Graphic/Graphic.coffee @@ -265,8 +265,23 @@ class Graphic.Text extends Graphic.Path @highlightIfNecessary(opts) toSvg: ({viewMatrix}) -> - # TODO - return "" + {text, fontFamily, textAlign, textBaseline, color} = @textComponent() + matrix = viewMatrix.compose(@matrix) + matrix = matrix.scale(1 / @textMultiplier, -1 / @textMultiplier) + text = Util.escapeHtml(text) + if textAlign == "left" + textAlign = "start" + else if textAlign == "center" + textAlign = "middle" + else if textAlign == "right" + textAlign = "end" + if textBaseline == "top" + textBaseline = "text-before-edge" + else if textBaseline == "bottom" + textBaseline = "text-after-edge" + return "#{text}" textComponent: -> @componentOfType(Graphic.TextComponent) diff --git a/src/Util/Matrix.coffee b/src/Util/Matrix.coffee index 3d7cb95..81c3403 100644 --- a/src/Util/Matrix.coffee +++ b/src/Util/Matrix.coffee @@ -51,8 +51,8 @@ module.exports = class Matrix [@e, @f] - toSVG: -> - "matrix(#{@m.join(" ")})" + toSvg: -> + "matrix(#{@a} #{@b} #{@c} #{@d} #{@e} #{@f})" canvasSetTransform: (ctx) -> ctx.setTransform(@a, @b, @c, @d, @e, @f) diff --git a/src/Util/Util.coffee b/src/Util/Util.coffee index 286edd4..57e4cbe 100644 --- a/src/Util/Util.coffee +++ b/src/Util/Util.coffee @@ -162,3 +162,8 @@ Util.isKeywordLiteral = (string) -> string == "true" or string == "false" or string == "null" or string == "undefined" ) + +Util.escapeHtml = (str) -> + div = document.createElement("div") + div.appendChild(document.createTextNode(str)) + return div.innerHTML From be3671156fec29d67d343d98b640f2540afd3ac8 Mon Sep 17 00:00:00 2001 From: Toby Schachman Date: Thu, 5 Jan 2017 14:42:13 -0800 Subject: [PATCH 4/6] Stand in for Graphics.Image's toSVG method --- src/Graphic/Graphic.coffee | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Graphic/Graphic.coffee b/src/Graphic/Graphic.coffee index 1174dbb..fbaf5b8 100644 --- a/src/Graphic/Graphic.coffee +++ b/src/Graphic/Graphic.coffee @@ -367,6 +367,10 @@ class Graphic.Image extends Graphic.Path imageCache.get(url, (image) => @drawImage(opts, image)) + toSvg: (opts) -> + # TODO + return "" + imageComponent: -> @componentOfType(Graphic.ImageComponent) From 644fe4fb80afafc2bc2015e350ec2c0a1db6a981 Mon Sep 17 00:00:00 2001 From: Toby Schachman Date: Thu, 5 Jan 2017 14:50:12 -0800 Subject: [PATCH 5/6] Factored out Editor.exportSvgString() --- src/Model/Editor.coffee | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Model/Editor.coffee b/src/Model/Editor.coffee index f5be30b..6c8480e 100644 --- a/src/Model/Editor.coffee +++ b/src/Model/Editor.coffee @@ -166,6 +166,11 @@ module.exports = class Editor # =========================================================================== exportSvg: -> + svgString = @exportSvgString() + fileName = @project.editingElement.label + ".svg" + Storage.saveFile(svgString, fileName, "image/svg+xml;charset=utf-8") + + exportSvgString: -> # These scaling parameters are arbitrary. They should be configurable when # you export. viewMatrix = new Util.Matrix(100, 0, 0, -100, 0, 0) # scale, flip Y @@ -175,9 +180,7 @@ module.exports = class Editor for graphic in graphics svgString += graphic.toSvg(opts) svgString += "" - - fileName = @project.editingElement.label + ".svg" - Storage.saveFile(svgString, fileName, "image/svg+xml;charset=utf-8") + return svgString # =========================================================================== From 82237c5b82038624292552d3d03a13f84e138814 Mon Sep 17 00:00:00 2001 From: Toby Schachman Date: Thu, 5 Jan 2017 15:08:23 -0800 Subject: [PATCH 6/6] added options to Editor.exportSvg as well as more useful defaults for laser cutting. --- src/Model/Editor.coffee | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/Model/Editor.coffee b/src/Model/Editor.coffee index 6c8480e..b34682d 100644 --- a/src/Model/Editor.coffee +++ b/src/Model/Editor.coffee @@ -165,20 +165,28 @@ module.exports = class Editor # Export # =========================================================================== - exportSvg: -> - svgString = @exportSvgString() + exportSvg: (opts) -> + svgString = @exportSvgString(opts) fileName = @project.editingElement.label + ".svg" Storage.saveFile(svgString, fileName, "image/svg+xml;charset=utf-8") - exportSvgString: -> - # These scaling parameters are arbitrary. They should be configurable when - # you export. - viewMatrix = new Util.Matrix(100, 0, 0, -100, 0, 0) # scale, flip Y - opts = {viewMatrix} + exportSvgString: (opts={}) -> + dpi = opts.dpi ? 100 + xMin = opts.xMin ? -6 + xMax = opts.xMax ? 6 + yMin = opts.yMin ? -6 + yMax = opts.yMax ? 6 + + # Note we flip vertically so the SVG has the same orientation as what's + # shown in the Apparatus canvas. + viewMatrix = new Util.Matrix(dpi, 0, 0, -dpi, -xMin*dpi, yMax*dpi) + width = (xMax-xMin) * dpi + height = (yMax-yMin) * dpi + graphics = @project.editingElement.allGraphics() - svgString = "" + svgString = "" for graphic in graphics - svgString += graphic.toSvg(opts) + svgString += graphic.toSvg({viewMatrix}) svgString += "" return svgString