Visitor Pattern and Exporters
Overview
The project uses the Visitor pattern to separate shape data from rendering and export logic. This allows adding new output formats without modifying shape classes.
ShapeVisitor Interface
public interface ShapeVisitor {
void visitRectangle(SRectangle sRectangle);
void visitCollection(SCollection collection);
void visitCircle(SCircle sCircle);
void visitTriangle(STriangle sTriangle);
void visitText(SText sText);
}
How It Works
Each Shape implements accept(ShapeVisitor visitor) by calling the appropriate visit* method on the visitor, passing itself:
// In SRectangle:
@Override
public void accept(ShapeVisitor visitor) {
visitor.visitRectangle(this);
}
For SCollection, the visitor iterates over children to traverse the tree:
// Inside a visitor's visitCollection():
for (Shape child : collection) {
child.accept(this);
}
This double-dispatch mechanism lets each visitor decide how to handle each shape type without the shapes knowing anything about rendering.
Visitor Implementations
1. ShapeDraftman -- GUI Rendering
Location: ovh.gasser.newshapes.ui.ShapeDraftman
Renders shapes onto a Graphics2D context for the Swing view (ShapesView).
Responsibilities:
- Draws rectangles (
fillRect/drawRect), circles (fillOval/drawOval), triangles (fillPolygon/drawPolygon), and text (drawString) usingGraphics2D. - Reads
ColorAttributesto determine fill/stroke colors and whether to fill or stroke. - Draws selection handles (small squares) when a shape is selected.
- In resize mode, renders all 8 directional handles (NW, N, NE, E, SE, S, SW, W) on the shape bounds.
- In normal mode, shows only the SE corner handle for selected shapes.
2. HTMLDraftman -- HTML/CSS Export
Location: ovh.gasser.newshapes.ui.visitors.HTMLDraftman
Generates HTML <div> elements with CSS for absolute positioning.
Output format:
| Shape | HTML Representation |
|---|---|
| Rectangle | <div> with position: absolute, left, top, width, height |
| Circle | <div> with border-radius: 50% |
| Triangle | <div> using CSS border tricks |
| Text | Styled <div> with font properties |
| Collection | Recursively visits children |
Colors are mapped to CSS background-color and border properties.
Used by: HTMLExporter, which writes out.html and style.css.
3. SVGDraftman -- SVG Export
Location: ovh.gasser.newshapes.ui.visitors.SVGDraftman
Generates SVG XML elements.
Output format:
| Shape | SVG Element |
|---|---|
| Rectangle | <rect x="..." y="..." width="..." height="..." fill="..." stroke="..."> |
| Circle | <circle cx="..." cy="..." r="..." fill="..." stroke="..."> |
| Triangle | <polygon points="..."> with computed vertex coordinates |
| Text | <text> with font attributes |
| Collection | <g> group containing child elements |
Color attributes are converted to SVG fill and stroke attributes using rgb(r,g,b) format.
Used by: SVGExporter, which writes out.svg.
Exporters
HTMLExporter
new HTMLExporter(model).export(); // Writes out.html + style.css
Creates an HTMLDraftman, visits the entire model tree, then writes the accumulated HTML structure and CSS styles to separate files.
SVGExporter
new SVGExporter(model).export(); // Writes out.svg
Creates an SVGDraftman, visits the entire model tree, wraps the output in SVG boilerplate (<svg> root element with namespace), and writes to a single file.
Adding a New Visitor / Exporter
Step 1: Create the Visitor
Create a class implementing ShapeVisitor (typically in ovh.gasser.newshapes.ui.visitors):
public class MyFormatDraftman implements ShapeVisitor {
@Override
public void visitRectangle(SRectangle rect) {
Rectangle bounds = rect.getBounds();
ColorAttributes colors = (ColorAttributes) rect.getAttributes(ColorAttributes.ID);
// Generate your format output...
}
@Override
public void visitCollection(SCollection collection) {
for (Shape child : collection) {
child.accept(this); // Recursive traversal
}
}
// ... implement visitCircle, visitTriangle, visitText
}
Step 2: Create the Exporter
public class MyFormatExporter {
private final SCollection model;
public MyFormatExporter(SCollection model) {
this.model = model;
}
public void export() throws FileNotFoundException {
MyFormatDraftman draftman = new MyFormatDraftman();
model.accept(draftman);
// Write draftman output to file(s)
}
}
Step 3: Wire It Up
Add a menu item in App.buildFileMenu():
JMenuItem myExportItem = new JMenuItem("Export to MyFormat");
myExportItem.addActionListener(evt -> {
try {
new MyFormatExporter(model).export();
} catch (FileNotFoundException e) {
logger.error("Could not export: {}", e.getMessage());
}
});
menuFile.add(myExportItem);
Known Issues
- HTMLDraftman.visitTriangle() uses
this.hashCode()instead ofsTriangle.hashCode()for CSS class naming. This is a bug that could cause CSS class collisions when multiple triangles are exported.