Architecture Documentation — Java Swing Shape Editor
1. Overview
This application is an interactive 2D shape editor built with Java Swing, structured around the
Model-View-Controller (MVC) architectural pattern. The model is a tree of shapes rooted at an
SCollection; the view renders that tree onto a JPanel using a Visitor; the controller translates
raw mouse and keyboard events into model mutations and triggers repaints.
The design deliberately separates concerns so that export targets (HTML, SVG) can reuse the same Visitor interface as the GUI renderer, and so that new shape types can be added with minimal changes to existing code.
2. Package Structure
ovh.gasser.newshapes
├── App.java — Entry point, builds JFrame, menu bar, initial model
├── Selection.java — Manages selected shapes, notifies listeners
├── ShapeVisitor.java — Visitor interface for shape rendering
├── HTMLExporter.java — Exports model to HTML/CSS files
├── SVGExporter.java — Exports model to SVG file
├── attributes/
│ ├── Attributes.java — Base interface (getID())
│ ├── ColorAttributes.java — Fill/stroke colors and flags
│ └── SelectionAttributes.java — Selection state
├── shapes/
│ ├── Shape.java — Core interface
│ ├── AbstractShape.java — Base implementation (bounds, attributes map, translate, resize)
│ ├── SRectangle.java — Rectangle shape
│ ├── SCircle.java — Circle shape (radius-based)
│ ├── STriangle.java — Equilateral triangle
│ ├── SText.java — Text shape with font properties
│ ├── SCollection.java — Composite pattern: group of shapes
│ └── ResizeHandle.java — Enum for 8 directional resize handles
├── ui/
│ ├── Controller.java — Mouse/keyboard event handler (MVC controller)
│ ├── ShapeDraftman.java — GUI rendering visitor (draws on Graphics2D)
│ ├── ShapesView.java — JPanel view, owns Controller
│ ├── listeners/
│ │ ├── MenuAddListener.java — Adds shapes from menu
│ │ ├── MenuEditListener.java — Color/border/fill changes
│ │ └── SelectionListener.java — Interface for selection change events
│ └── visitors/
│ ├── HTMLDraftman.java — Generates HTML/CSS from shapes
│ └── SVGDraftman.java — Generates SVG from shapes
└── util/
└── Streamable.java — Adds stream() to Iterables
3. MVC Breakdown
Roles
| Layer | Class(es) | Responsibility |
|---|---|---|
| Model | SCollection, Shape, AbstractShape, subclasses |
Shape tree; single source of truth for geometry and attributes |
| View | ShapesView |
JPanel subclass; delegates all painting to ShapeDraftman |
| Controller | Controller |
Receives Swing events; mutates model; triggers repaint |
Data Flow
User input (mouse / keyboard)
|
v
Controller
(MouseListener, KeyListener)
|
|-- hit-test shapes in model
|-- mutate model (translate, resize, add, remove)
|-- update Selection
|
v
Model mutation complete
|
v
ShapesView.repaint()
|
v
ShapesView.paintComponent(Graphics g)
|
v
ShapeDraftman.visit*(shape) [Visitor traversal of the shape tree]
|
v
Graphics2D rendering on screen
The view never writes to the model. The model has no reference to the view or the controller. Communication from model to view is limited to the repaint trigger issued by the controller after each mutation.
4. Design Patterns
4.1 Visitor Pattern
ShapeVisitor is the central extension point for operations over the shape tree.
public interface ShapeVisitor {
void visitRectangle(SRectangle rectangle);
void visitCircle(SCircle circle);
void visitTriangle(STriangle triangle);
void visitText(SText text);
void visitCollection(SCollection collection);
}
Each concrete shape implements accept(ShapeVisitor v) by calling the appropriate method on the
visitor:
// SRectangle.java
@Override
public void accept(ShapeVisitor v) {
v.visitRectangle(this);
}
Three concrete visitors are provided:
| Visitor | Purpose |
|---|---|
ShapeDraftman |
Paints shapes onto a Graphics2D context for on-screen rendering |
HTMLDraftman |
Emits HTML <div> elements with inline CSS for each shape |
SVGDraftman |
Emits SVG primitives (<rect>, <circle>, <polygon>, <text>) |
Adding a new rendering target requires only a new class implementing ShapeVisitor; no existing
shape or visitor code changes.
4.2 Composite Pattern
SCollection extends AbstractShape and holds an ordered list of Shape children, allowing
shapes to be grouped and treated uniformly.
Shape (interface)
└── AbstractShape (abstract)
├── SRectangle
├── SCircle
├── STriangle
├── SText
└── SCollection <-- contains List<Shape> children
├── SRectangle
├── SCircle
└── SCollection <-- groups can be nested
Key overrides in SCollection:
translate(dx, dy)— propagates the translation to every childgetBounds()— returns the union of all children's bounding rectanglesclone()— performs a deep copy by cloning each child recursivelyaccept(ShapeVisitor)— callsvisitCollection, which internally iterates children
SCollection also implements Streamable<Shape>, providing a stream() method that exposes the
child list as a Stream<Shape> for functional-style iteration.
4.3 Observer Pattern
Selection maintains the set of currently selected shapes and a list of SelectionListener
callbacks.
Selection
├── List<Shape> selectedShapes
├── List<SelectionListener> listeners
├── add(Shape) → notifyListeners()
├── clear() → notifyListeners()
└── notifyListeners()
SelectionListener (interface)
└── onSelectionChanged(Selection selection)
App implements SelectionListener
└── updates menu checkbox state (fill / border toggles)
When the selection changes, App reads the ColorAttributes of the newly selected shapes and
synchronises the menu checkboxes accordingly, so the UI always reflects the state of the model.
4.4 Factory Methods
Each concrete shape exposes a static create() method to centralise construction and attribute
initialisation:
SRectangle.create(int x, int y, int width, int height, Color color)
SCircle.create(int x, int y, int radius)
STriangle.create(int x, int y, int side)
SText.create(int x, int y, String text, Font font, Color color)
This keeps instantiation logic off the caller and ensures every shape is created with a consistent
default attribute set (a ColorAttributes and a SelectionAttributes are always added to the
internal TreeMap during create()).
5. Selection and Interaction Model
Mouse Interactions
| Gesture | Effect |
|---|---|
| Click on shape | Select that shape; clear previous selection |
| Shift + click on shape | Add shape to current selection (multi-select) |
| Click on empty space | Deselect all |
| Click on empty space + drag | Rubber-band box selection: selects all shapes whose bounding rectangle intersects the drag rectangle |
| Drag selected shape(s) | Translate all selected shapes by the drag delta |
Keyboard Interactions
| Key | Effect |
|---|---|
R |
Toggle resize mode for the current selection |
Delete / Backspace |
Remove selected shapes from the model |
Resize Mode
When resize mode is active, the ShapeDraftman renders eight handles around each selected shape,
corresponding to the eight values of the ResizeHandle enum:
NW --- N --- NE
| |
W E
| |
SW --- S --- SE
Dragging a handle resizes the shape in the direction indicated by the handle, updating the bounding rectangle while keeping the opposite corner anchored.
6. Attributes System
Shapes store their attributes in a TreeMap<String, Attributes> where the key is the stable ID
string returned by Attributes.getID(). This map lives in AbstractShape and is inherited by all
concrete shapes.
AbstractShape
└── TreeMap<String, Attributes> attributes
├── "COLOR_ATTRS" → ColorAttributes
└── "SELECTION_ATTRS" → SelectionAttributes
Defined Attribute Types
ColorAttributes (ID = "COLOR_ATTRS")
| Field | Type | Description |
|---|---|---|
fillColor |
Color |
Interior fill colour |
strokeColor |
Color |
Border/outline colour |
filled |
boolean |
Whether the shape is filled |
stroked |
boolean |
Whether the shape has a visible border |
SelectionAttributes (ID = "SELECTION_ATTRS")
| Field | Type | Description |
|---|---|---|
selected |
boolean |
Whether the shape is currently selected |
Extensibility
The attribute system is open for extension. A new attribute type — for example, OpacityAttributes
or ShadowAttributes — only needs to implement the Attributes interface and provide a unique ID
string. No existing attribute class or shape class requires modification.
public interface Attributes {
String getID();
}
Consumers retrieve attributes with a cast after a key lookup, making it straightforward to check for the presence of an optional attribute type without imposing it on all shapes.