From b8ecf9859b719527cdcf46d8e78b23d0393e9ac4 Mon Sep 17 00:00:00 2001 From: Thibaud Date: Thu, 19 Mar 2026 19:06:12 +0100 Subject: [PATCH] Implement SVG export --- src/main/java/ovh/gasser/newshapes/App.java | 10 ++- .../ovh/gasser/newshapes/HTMLExporter.java | 4 +- .../ovh/gasser/newshapes/SVGExporter.java | 36 ++++++++ .../ui/{html => visitors}/HTMLDraftman.java | 2 +- .../newshapes/ui/visitors/SVGDraftman.java | 82 +++++++++++++++++++ 5 files changed, 130 insertions(+), 4 deletions(-) create mode 100644 src/main/java/ovh/gasser/newshapes/SVGExporter.java rename src/main/java/ovh/gasser/newshapes/ui/{html => visitors}/HTMLDraftman.java (98%) create mode 100644 src/main/java/ovh/gasser/newshapes/ui/visitors/SVGDraftman.java diff --git a/src/main/java/ovh/gasser/newshapes/App.java b/src/main/java/ovh/gasser/newshapes/App.java index 4424097..e9d872b 100644 --- a/src/main/java/ovh/gasser/newshapes/App.java +++ b/src/main/java/ovh/gasser/newshapes/App.java @@ -15,7 +15,6 @@ import ovh.gasser.newshapes.ui.listeners.SelectionListener; import javax.swing.*; import java.awt.*; import java.io.FileNotFoundException; -import java.util.Optional; public class App { private final static Logger logger = LoggerFactory.getLogger(App.class); @@ -82,6 +81,7 @@ public class App { JMenuItem addRectItem = new JMenuItem("Add SRectangle"); JMenuItem addCircleItem = new JMenuItem("Add SCircle"); JMenuItem htmlExportItem = new JMenuItem("Export to HTML"); + JMenuItem svgExportItem = new JMenuItem("Export to SVG"); JMenuItem exitItem = new JMenuItem("Exit"); addRectItem.addActionListener(new MenuAddListener("SRectangle", model, sview)); addCircleItem.addActionListener(new MenuAddListener("SCircle", model, sview)); @@ -92,11 +92,19 @@ public class App { logger.error("Could not export as html: {}", e.getMessage()); } }); + svgExportItem.addActionListener(evt -> { + try { + new SVGExporter(model).export(); + } catch (FileNotFoundException e) { + logger.error("Could not export as html: {}", e.getMessage()); + } + }); exitItem.addActionListener(evt -> System.exit(0)); menuFile.add(addRectItem); menuFile.add(addCircleItem); menuFile.addSeparator(); menuFile.add(htmlExportItem); + menuFile.add(svgExportItem); menuFile.add(exitItem); return menuFile; } diff --git a/src/main/java/ovh/gasser/newshapes/HTMLExporter.java b/src/main/java/ovh/gasser/newshapes/HTMLExporter.java index c24f3d2..6d53dfb 100644 --- a/src/main/java/ovh/gasser/newshapes/HTMLExporter.java +++ b/src/main/java/ovh/gasser/newshapes/HTMLExporter.java @@ -2,7 +2,7 @@ package ovh.gasser.newshapes; import ovh.gasser.newshapes.shapes.SCollection; import ovh.gasser.newshapes.shapes.SRectangle; -import ovh.gasser.newshapes.ui.html.*; +import ovh.gasser.newshapes.ui.visitors.*; import java.awt.*; import java.io.FileNotFoundException; @@ -12,7 +12,7 @@ public class HTMLExporter { private final SCollection model; - private HTMLExporter() throws FileNotFoundException { + private HTMLExporter() { this(SCollection.of( SRectangle.create(10, 10, 40, 60, Color.RED), SRectangle.create(70, 10, 40, 60), diff --git a/src/main/java/ovh/gasser/newshapes/SVGExporter.java b/src/main/java/ovh/gasser/newshapes/SVGExporter.java new file mode 100644 index 0000000..1b2ce58 --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/SVGExporter.java @@ -0,0 +1,36 @@ +package ovh.gasser.newshapes; + +import ovh.gasser.newshapes.attributes.ColorAttributes; +import ovh.gasser.newshapes.shapes.SCircle; +import ovh.gasser.newshapes.shapes.SCollection; +import ovh.gasser.newshapes.ui.visitors.SVGDraftman; + +import java.awt.*; +import java.io.FileNotFoundException; +import java.io.PrintWriter; + +public class SVGExporter { + + private final SCollection model; + + public SVGExporter() { + SCircle circle = SCircle.create(200, 100, 50); + circle.addAttributes(new ColorAttributes(true, true, Color.LIGHT_GRAY, Color.BLACK)); + this.model = SCollection.of(circle); + } + + public SVGExporter(SCollection model) { + this.model = model; + } + + public void export() throws FileNotFoundException { + try (final PrintWriter svg = new PrintWriter("out.svg")) { + SVGDraftman draftman = new SVGDraftman(svg); + draftman.generateSVG(this.model); + } + } + + public static void main(String[] args) throws FileNotFoundException { + new SVGExporter().export(); + } +} diff --git a/src/main/java/ovh/gasser/newshapes/ui/html/HTMLDraftman.java b/src/main/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftman.java similarity index 98% rename from src/main/java/ovh/gasser/newshapes/ui/html/HTMLDraftman.java rename to src/main/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftman.java index 062a47f..637da66 100644 --- a/src/main/java/ovh/gasser/newshapes/ui/html/HTMLDraftman.java +++ b/src/main/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftman.java @@ -1,4 +1,4 @@ -package ovh.gasser.newshapes.ui.html; +package ovh.gasser.newshapes.ui.visitors; import ovh.gasser.newshapes.ShapeVisitor; import ovh.gasser.newshapes.attributes.ColorAttributes; diff --git a/src/main/java/ovh/gasser/newshapes/ui/visitors/SVGDraftman.java b/src/main/java/ovh/gasser/newshapes/ui/visitors/SVGDraftman.java new file mode 100644 index 0000000..202ce81 --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/ui/visitors/SVGDraftman.java @@ -0,0 +1,82 @@ +package ovh.gasser.newshapes.ui.visitors; + +import ovh.gasser.newshapes.App; +import ovh.gasser.newshapes.ShapeVisitor; +import ovh.gasser.newshapes.attributes.ColorAttributes; +import ovh.gasser.newshapes.shapes.SCircle; +import ovh.gasser.newshapes.shapes.SCollection; +import ovh.gasser.newshapes.shapes.SRectangle; + +import java.awt.*; +import java.io.PrintWriter; + +public class SVGDraftman implements ShapeVisitor { + private static final String SVG_PRELUDE = """ + + + """; + private static final String SVG_POSTLUDE = ""; + + private final PrintWriter output; + + public SVGDraftman(PrintWriter output) { + this.output = output; + } + + @Override + public void visitRectangle(SRectangle sRectangle) { + int x = sRectangle.getBounds().x; + int y = sRectangle.getBounds().y; + int w = sRectangle.getBounds().width; + int h = sRectangle.getBounds().height; + ColorAttributes attrs = (ColorAttributes) sRectangle.getAttributes(ColorAttributes.ID); + this.output.println(String.format(""" + """, + w, h, x, y, buildColorParameters(attrs))); + } + + @Override + public void visitCollection(SCollection collection) { + collection.stream().forEach(shape -> shape.accept(this)); + } + + @Override + public void visitCircle(SCircle sCircle) { + int r = sCircle.getRadius(); + int x = sCircle.getBounds().x + r; + int y = sCircle.getBounds().y + r; + var attrs = (ColorAttributes) sCircle.getAttributes(ColorAttributes.ID); + this.output.println(String.format(""" + """, + x, y, r, buildColorParameters(attrs))); + } + + public void generateSVG(SCollection model) { + output.println(String.format(SVG_PRELUDE, App.WIN_SIZE.width, App.WIN_SIZE.height)); + visitCollection(model); + output.println(SVG_POSTLUDE); + } + + private String colorToHex(Color c) { + int r = c.getRed(); + int g = c.getGreen(); + int b = c.getBlue(); + return String.format("#%02x%02x%02x", r, g, b); + } + + private StringBuilder buildColorParameters(ColorAttributes attrs){ + var params = new StringBuilder(); + if (attrs.stroked) { + params.append(String.format("stroke=\"%s\" stroke-width=\"1\"", colorToHex(attrs.strokedColor))); + params.append(" "); + } + if (attrs.filled) { + params.append(String.format("fill=\"%s\"", colorToHex(attrs.filledColor))); + params.append(" "); + } else { + params.append(String.format("fill=\"%s\"", "none")); + params.append(" "); + } + return params; + } +}