diff --git a/new-shapes.wiki b/new-shapes.wiki new file mode 160000 index 0000000..731a57b --- /dev/null +++ b/new-shapes.wiki @@ -0,0 +1 @@ +Subproject commit 731a57b080f9baa4d03ba3513d642345b410beaa diff --git a/src/main/java/ovh/gasser/newshapes/ShapeVisitor.java b/src/main/java/ovh/gasser/newshapes/ShapeVisitor.java index 2362f52..025f6d9 100644 --- a/src/main/java/ovh/gasser/newshapes/ShapeVisitor.java +++ b/src/main/java/ovh/gasser/newshapes/ShapeVisitor.java @@ -2,6 +2,7 @@ package ovh.gasser.newshapes; import ovh.gasser.newshapes.shapes.SCircle; import ovh.gasser.newshapes.shapes.SCollection; +import ovh.gasser.newshapes.shapes.SPolygon; import ovh.gasser.newshapes.shapes.SRectangle; import ovh.gasser.newshapes.shapes.SText; import ovh.gasser.newshapes.shapes.STriangle; @@ -16,4 +17,6 @@ public interface ShapeVisitor { void visitTriangle(STriangle sTriangle); void visitText(SText sText); + + void visitPolygon(SPolygon sPolygon); } diff --git a/src/main/java/ovh/gasser/newshapes/shapes/SPolygon.java b/src/main/java/ovh/gasser/newshapes/shapes/SPolygon.java new file mode 100644 index 0000000..aff1040 --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/shapes/SPolygon.java @@ -0,0 +1,110 @@ +package ovh.gasser.newshapes.shapes; + +import ovh.gasser.newshapes.ShapeVisitor; +import ovh.gasser.newshapes.attributes.ColorAttributes; +import ovh.gasser.newshapes.attributes.SelectionAttributes; + +import java.awt.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class SPolygon extends AbstractShape { + private final List points; + + private SPolygon(List points) { + super(calculateBounds(points)); + this.points = new ArrayList<>(points); + } + + @Override + public void accept(ShapeVisitor visitor) { + visitor.visitPolygon(this); + } + + @Override + public void translate(int dx, int dy) { + super.translate(dx, dy); + for (Point p : points) { + p.translate(dx, dy); + } + } + + @Override + public void resize(ResizeHandle handle, int dx, int dy) { + Rectangle oldBounds = getBounds(); + super.resize(handle, dx, dy); + Rectangle newBounds = getBounds(); + + if (oldBounds.width == 0 || oldBounds.height == 0) { + return; + } + + double scaleX = (double) newBounds.width / oldBounds.width; + double scaleY = (double) newBounds.height / oldBounds.height; + + for (Point p : points) { + int relativeX = p.x - oldBounds.x; + int relativeY = p.y - oldBounds.y; + p.x = newBounds.x + (int) (relativeX * scaleX); + p.y = newBounds.y + (int) (relativeY * scaleY); + } + } + + @Override + public Shape clone() { + List clonedPoints = points.stream() + .map(p -> new Point(p.x, p.y)) + .toList(); + SPolygon clone = new SPolygon(clonedPoints); + ColorAttributes color = (ColorAttributes) this.getAttributes(ColorAttributes.ID); + if (color != null) { + clone.addAttributes(new ColorAttributes(color.filled, color.stroked, color.filledColor, color.strokedColor)); + } + clone.addAttributes(new SelectionAttributes()); + return clone; + } + + public List getPoints() { + return Collections.unmodifiableList(points); + } + + public static SPolygon create(List points) { + if (points == null || points.size() < 3) { + throw new IllegalArgumentException("Polygon must have at least 3 points"); + } + SPolygon polygon = new SPolygon(points); + polygon.addAttributes(new SelectionAttributes()); + polygon.addAttributes(new ColorAttributes(false, true, Color.BLACK, Color.BLACK)); + return polygon; + } + + public static SPolygon create(Point... points) { + return create(List.of(points)); + } + + private static Rectangle calculateBounds(List points) { + if (points == null || points.isEmpty()) { + return new Rectangle(0, 0, 0, 0); + } + + int minX = points.get(0).x; + int minY = points.get(0).y; + int maxX = points.get(0).x; + int maxY = points.get(0).y; + + for (Point p : points) { + minX = Math.min(minX, p.x); + minY = Math.min(minY, p.y); + maxX = Math.max(maxX, p.x); + maxY = Math.max(maxY, p.y); + } + + return new Rectangle(minX, minY, maxX - minX, maxY - minY); + } + + @Override + public String toString() { + return "SPolygon{points=" + points.size() + ", bounds=" + super.toString() + "}"; + } +} diff --git a/src/main/java/ovh/gasser/newshapes/ui/ShapeDraftman.java b/src/main/java/ovh/gasser/newshapes/ui/ShapeDraftman.java index 766531d..549667b 100644 --- a/src/main/java/ovh/gasser/newshapes/ui/ShapeDraftman.java +++ b/src/main/java/ovh/gasser/newshapes/ui/ShapeDraftman.java @@ -117,6 +117,30 @@ public class ShapeDraftman implements ShapeVisitor { drawHandlerIfSelected(text); } + @Override + public void visitPolygon(SPolygon polygon) { + ColorAttributes colAttrs = (ColorAttributes) polygon.getAttributes(ColorAttributes.ID); + if (colAttrs == null) { + colAttrs = DEFAULT_COLOR_ATTRIBUTES; + } + + java.util.List points = polygon.getPoints(); + int[] xPoints = points.stream().mapToInt(p -> p.x).toArray(); + int[] yPoints = points.stream().mapToInt(p -> p.y).toArray(); + int nPoints = points.size(); + + if (colAttrs.filled) { + this.g2d.setColor(colAttrs.filledColor); + this.g2d.fillPolygon(xPoints, yPoints, nPoints); + } + if (colAttrs.stroked) { + this.g2d.setColor(colAttrs.strokedColor); + this.g2d.drawPolygon(xPoints, yPoints, nPoints); + } + + drawHandlerIfSelected(polygon); + } + private Color resolveTextColor(ColorAttributes attrs) { if (attrs == null) { return Color.BLACK; diff --git a/src/main/java/ovh/gasser/newshapes/ui/listeners/MenuAddListener.java b/src/main/java/ovh/gasser/newshapes/ui/listeners/MenuAddListener.java index 231542c..923be67 100644 --- a/src/main/java/ovh/gasser/newshapes/ui/listeners/MenuAddListener.java +++ b/src/main/java/ovh/gasser/newshapes/ui/listeners/MenuAddListener.java @@ -19,11 +19,17 @@ public class MenuAddListener implements ActionListener { private final String shape; private final ShapesView view; private final SCollection model; + private final Runnable onModelChanged; public MenuAddListener(String shape, SCollection model, ShapesView view) { + this(shape, model, view, () -> { }); + } + + public MenuAddListener(String shape, SCollection model, ShapesView view, Runnable onModelChanged) { this.shape = shape; this.model = model; this.view = view; + this.onModelChanged = onModelChanged; } @Override public void actionPerformed(ActionEvent e) { @@ -41,6 +47,7 @@ public class MenuAddListener implements ActionListener { s.addAttributes(new SelectionAttributes()); s.addAttributes(new ColorAttributes(true, false, randomColor(), Color.BLACK)); model.add(s); + onModelChanged.run(); view.repaint(); } diff --git a/src/main/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftman.java b/src/main/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftman.java index ea376e9..6b6dcae 100644 --- a/src/main/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftman.java +++ b/src/main/java/ovh/gasser/newshapes/ui/visitors/HTMLDraftman.java @@ -2,7 +2,12 @@ package ovh.gasser.newshapes.ui.visitors; import ovh.gasser.newshapes.ShapeVisitor; import ovh.gasser.newshapes.attributes.ColorAttributes; -import ovh.gasser.newshapes.shapes.*; +import ovh.gasser.newshapes.shapes.SCircle; +import ovh.gasser.newshapes.shapes.SCollection; +import ovh.gasser.newshapes.shapes.SPolygon; +import ovh.gasser.newshapes.shapes.SRectangle; +import ovh.gasser.newshapes.shapes.SText; +import ovh.gasser.newshapes.shapes.STriangle; import ovh.gasser.newshapes.shapes.Shape; import java.awt.*; @@ -105,6 +110,36 @@ public class HTMLDraftman implements ShapeVisitor { cssOutput.println("}"); } + @Override + public void visitPolygon(SPolygon polygon) { + int id = polygon.hashCode(); + Rectangle bounds = polygon.getBounds(); + htmlOutput.printf("\n", id, bounds.x, bounds.y, bounds.width, bounds.height); + + var points = new StringJoiner(" "); + for (var point : polygon.getPoints()) { + points.add("%d,%d".formatted(point.x, point.y)); + } + + ColorAttributes attrs = (ColorAttributes) polygon.getAttributes(ColorAttributes.ID); + String fill = attrs.filled ? formatCSSColor(attrs.filledColor) : "none"; + String stroke = attrs.stroked ? formatCSSColor(attrs.strokedColor) : "none"; + int strokeWidth = attrs.stroked ? 1 : 0; + + htmlOutput.printf(" \n", + points, fill, stroke, strokeWidth); + htmlOutput.println(""); + + cssOutput.printf(".poly%d{\n", id); + cssOutput.println(" position: absolute;"); + cssOutput.printf(" top: %dpx;\n", bounds.y); + cssOutput.printf(" left: %dpx;\n", bounds.x); + cssOutput.printf(" width: %dpx;\n", bounds.width); + cssOutput.printf(" height: %dpx;\n", bounds.height); + cssOutput.println(" overflow: visible;"); + cssOutput.println("}"); + } + private String attributesToCss(Shape shape) { ColorAttributes attrs = (ColorAttributes) shape.getAttributes(ColorAttributes.ID); String strokedColor = "#ffffff"; diff --git a/src/main/java/ovh/gasser/newshapes/ui/visitors/SVGDraftman.java b/src/main/java/ovh/gasser/newshapes/ui/visitors/SVGDraftman.java index 3ffc0e4..bb77df1 100644 --- a/src/main/java/ovh/gasser/newshapes/ui/visitors/SVGDraftman.java +++ b/src/main/java/ovh/gasser/newshapes/ui/visitors/SVGDraftman.java @@ -5,6 +5,7 @@ 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.SPolygon; import ovh.gasser.newshapes.shapes.SRectangle; import ovh.gasser.newshapes.shapes.SText; import ovh.gasser.newshapes.shapes.STriangle; @@ -92,6 +93,21 @@ public class SVGDraftman implements ShapeVisitor { ); } + @Override + public void visitPolygon(SPolygon sPolygon) { + var points = new StringJoiner(" "); + for (var point : sPolygon.getPoints()) { + points.add("%d,%d".formatted(point.x, point.y)); + } + ColorAttributes attrs = (ColorAttributes) sPolygon.getAttributes(ColorAttributes.ID); + var style = new StringJoiner(";", "fill:", ""); + style.add(attrs.filled ? colorToHex(attrs.filledColor) : "none"); + if (attrs.stroked) { + style.add("stroke:%s;stroke-width:1".formatted(colorToHex(attrs.strokedColor))); + } + this.output.printf("\n", points, style); + } + public void generateSVG(SCollection model) { output.println(String.format(SVG_PRELUDE, App.WIN_SIZE.width, App.WIN_SIZE.height)); visitCollection(model);