add generated docs
414
API.md
Normal file
414
API.md
Normal file
@@ -0,0 +1,414 @@
|
|||||||
|
# API Reference
|
||||||
|
|
||||||
|
This document covers the public API for the Java shape editor library (`ovh.gasser.newshapes`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Shape (interface)](#shape-interface)
|
||||||
|
- [AbstractShape (abstract class)](#abstractshape-abstract-class)
|
||||||
|
- [Concrete Shapes](#concrete-shapes)
|
||||||
|
- [SRectangle](#srectangle)
|
||||||
|
- [SCircle](#scircle)
|
||||||
|
- [STriangle](#striangle)
|
||||||
|
- [SText](#stext)
|
||||||
|
- [SCollection](#scollection)
|
||||||
|
- [ShapeVisitor (interface)](#shapevisitor-interface)
|
||||||
|
- [Attributes (interface)](#attributes-interface)
|
||||||
|
- [ColorAttributes](#colorattributes)
|
||||||
|
- [SelectionAttributes](#selectionattributes)
|
||||||
|
- [Selection (class)](#selection-class)
|
||||||
|
- [ResizeHandle (enum)](#resizehandle-enum)
|
||||||
|
- [Streamable (interface)](#streamable-interface)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Shape (interface)
|
||||||
|
|
||||||
|
**Package:** `ovh.gasser.newshapes.shapes`
|
||||||
|
|
||||||
|
The root interface for all drawable shapes. Every shape in the model implements this interface.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public interface Shape {
|
||||||
|
void accept(ShapeVisitor visitor);
|
||||||
|
void translate(int dx, int dy);
|
||||||
|
void resize(ResizeHandle handle, int dx, int dy);
|
||||||
|
Attributes getAttributes(String key);
|
||||||
|
void addAttributes(Attributes attr);
|
||||||
|
Rectangle getBounds();
|
||||||
|
Shape clone();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Method Descriptions
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `accept(ShapeVisitor visitor)` | Accepts a visitor, enabling rendering, export, or other operations via the Visitor pattern. |
|
||||||
|
| `translate(int dx, int dy)` | Moves the shape by the given delta along each axis. |
|
||||||
|
| `resize(ResizeHandle handle, int dx, int dy)` | Resizes the shape by dragging the specified handle by the given delta. |
|
||||||
|
| `getAttributes(String key)` | Returns the `Attributes` instance stored under the given key, or `null` if absent. |
|
||||||
|
| `addAttributes(Attributes attr)` | Adds or replaces the attribute entry keyed by `attr.getID()`. |
|
||||||
|
| `getBounds()` | Returns a defensive copy of the shape's bounding rectangle. |
|
||||||
|
| `clone()` | Returns a deep copy of the shape. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AbstractShape (abstract class)
|
||||||
|
|
||||||
|
**Package:** `ovh.gasser.newshapes.shapes`
|
||||||
|
|
||||||
|
Base implementation of `Shape`. Provides shared attribute storage and default implementations of `translate`, `resize`, and `getBounds`. Subclasses must implement `accept` and `clone`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public abstract class AbstractShape implements Shape {
|
||||||
|
protected final Rectangle bounds;
|
||||||
|
// Attributes stored in a TreeMap<String, Attributes>
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void translate(int dx, int dy);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resize(ResizeHandle handle, int dx, int dy);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Rectangle getBounds();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public abstract Shape clone();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Implementation Notes
|
||||||
|
|
||||||
|
- **`translate()`** — Delegates directly to `bounds.translate(dx, dy)`.
|
||||||
|
- **`resize()`** — Switches on the `ResizeHandle` enum to adjust the bounds `x`, `y`, `width`, or `height` fields accordingly. Enforces a minimum value of `1` for both width and height.
|
||||||
|
- **`getBounds()`** — Returns `new Rectangle(this.bounds)` to prevent external mutation of internal state.
|
||||||
|
- **`clone()`** — Abstract; each concrete subclass is responsible for providing a correct deep copy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Concrete Shapes
|
||||||
|
|
||||||
|
### SRectangle
|
||||||
|
|
||||||
|
**Package:** `ovh.gasser.newshapes.shapes`
|
||||||
|
|
||||||
|
Represents a rectangular shape. Extends `AbstractShape`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class SRectangle extends AbstractShape {
|
||||||
|
|
||||||
|
public static SRectangle create(int x, int y, int width, int height);
|
||||||
|
|
||||||
|
public static SRectangle create(int x, int y, int width, int height, Color color);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Factory Method | Description |
|
||||||
|
|----------------|-------------|
|
||||||
|
| `create(x, y, width, height)` | Creates a rectangle with no fill color. |
|
||||||
|
| `create(x, y, width, height, color)` | Creates a rectangle with the given fill color. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SCircle
|
||||||
|
|
||||||
|
**Package:** `ovh.gasser.newshapes.shapes`
|
||||||
|
|
||||||
|
Represents a circular shape. Extends `AbstractShape` and overrides `resize` and `getBounds` to maintain circular geometry.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class SCircle extends AbstractShape {
|
||||||
|
|
||||||
|
public static SCircle create(int x, int y, int radius);
|
||||||
|
|
||||||
|
public static SCircle create(int x, int y, int radius, Color fillColor);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resize(ResizeHandle handle, int dx, int dy);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Rectangle getBounds();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Factory Method | Description |
|
||||||
|
|----------------|-------------|
|
||||||
|
| `create(x, y, radius)` | Creates a circle with no fill color. |
|
||||||
|
| `create(x, y, radius, fillColor)` | Creates a circle with the given fill color. |
|
||||||
|
|
||||||
|
**Override Notes**
|
||||||
|
|
||||||
|
- **`resize()`** — Adjusts the radius based on the drag delta while keeping the shape circular.
|
||||||
|
- **`getBounds()`** — Computes the bounding rectangle from the stored center point and radius rather than a raw `Rectangle`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### STriangle
|
||||||
|
|
||||||
|
**Package:** `ovh.gasser.newshapes.shapes`
|
||||||
|
|
||||||
|
Represents an equilateral triangle. Extends `AbstractShape` and overrides `resize` to preserve equilateral proportions.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class STriangle extends AbstractShape {
|
||||||
|
|
||||||
|
public static STriangle create(int x, int y, int size, Color fillColor, Color strokeColor);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void resize(ResizeHandle handle, int dx, int dy);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Factory Method | Description |
|
||||||
|
|----------------|-------------|
|
||||||
|
| `create(x, y, size, fillColor, strokeColor)` | Creates an equilateral triangle with explicit fill and stroke colors. |
|
||||||
|
|
||||||
|
**Override Notes**
|
||||||
|
|
||||||
|
- **`resize()`** — Ensures that resizing keeps all sides equal, preserving the equilateral property.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SText
|
||||||
|
|
||||||
|
**Package:** `ovh.gasser.newshapes.shapes`
|
||||||
|
|
||||||
|
Represents an editable text label. Extends `AbstractShape`. When the stored text is `null` or empty, the shape renders a `"Click to edit"` placeholder.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class SText extends AbstractShape {
|
||||||
|
|
||||||
|
public static SText create(int x, int y, String text);
|
||||||
|
|
||||||
|
public static SText create(int x, int y, String text, String fontName, int fontStyle, int fontSize);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Factory Method | Description |
|
||||||
|
|----------------|-------------|
|
||||||
|
| `create(x, y, text)` | Creates a text shape using the default font: SansSerif, PLAIN, size 16. |
|
||||||
|
| `create(x, y, text, fontName, fontStyle, fontSize)` | Creates a text shape with a fully specified font. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SCollection
|
||||||
|
|
||||||
|
**Package:** `ovh.gasser.newshapes.shapes`
|
||||||
|
|
||||||
|
A composite shape that holds an ordered collection of child `Shape` instances. Implements both `Streamable<Shape>` and `Iterable<Shape>`. Overrides core `Shape` methods to propagate operations to all children.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class SCollection extends AbstractShape
|
||||||
|
implements Streamable<Shape>, Iterable<Shape> {
|
||||||
|
|
||||||
|
public static SCollection of(Shape... shapes);
|
||||||
|
|
||||||
|
public void add(Shape s);
|
||||||
|
|
||||||
|
public void remove(Shape s);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Rectangle getBounds();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Shape clone();
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void translate(int dx, int dy);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void addAttributes(Attributes attr);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Member | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `of(Shape... shapes)` | Factory method. Creates a collection from the given shapes and automatically adds a `SelectionAttributes` instance. |
|
||||||
|
| `add(Shape s)` | Adds a child shape to the collection. |
|
||||||
|
| `remove(Shape s)` | Removes a child shape from the collection. |
|
||||||
|
| `getBounds()` | Returns the union of all children's bounding rectangles. Returns the window size if the collection is empty. |
|
||||||
|
| `clone()` | Deep-copies the collection by recursively cloning each child. |
|
||||||
|
| `translate()` | Propagates the translation to every child shape. |
|
||||||
|
| `addAttributes()` | Propagates `ColorAttributes` to all children; other attribute types are stored locally. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ShapeVisitor (interface)
|
||||||
|
|
||||||
|
**Package:** `ovh.gasser.newshapes`
|
||||||
|
|
||||||
|
Visitor interface for operations over the shape hierarchy (rendering, export, hit-testing, etc.). Each concrete shape calls the appropriate `visit` method from its `accept` implementation.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public interface ShapeVisitor {
|
||||||
|
void visitRectangle(SRectangle sRectangle);
|
||||||
|
void visitCollection(SCollection collection);
|
||||||
|
void visitCircle(SCircle sCircle);
|
||||||
|
void visitTriangle(STriangle sTriangle);
|
||||||
|
void visitText(SText sText);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Attributes (interface)
|
||||||
|
|
||||||
|
**Package:** `ovh.gasser.newshapes.attributes`
|
||||||
|
|
||||||
|
Marker interface for metadata that can be attached to any `Shape`. All attribute types must provide a unique string key via `getID()`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public interface Attributes {
|
||||||
|
String getID();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ColorAttributes
|
||||||
|
|
||||||
|
**Package:** `ovh.gasser.newshapes.attributes`
|
||||||
|
|
||||||
|
Stores fill and stroke color information for a shape.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class ColorAttributes implements Attributes {
|
||||||
|
|
||||||
|
public static final String ID = "COLOR_ATTRS";
|
||||||
|
|
||||||
|
public boolean filled;
|
||||||
|
public boolean stroked;
|
||||||
|
public Color filledColor;
|
||||||
|
public Color strokedColor;
|
||||||
|
|
||||||
|
public ColorAttributes(boolean filled, boolean stroked,
|
||||||
|
Color filledColor, Color strokedColor);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getID(); // returns "COLOR_ATTRS"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Field | Description |
|
||||||
|
|-------|-------------|
|
||||||
|
| `filled` | Whether the shape interior is filled. |
|
||||||
|
| `stroked` | Whether the shape outline is drawn. |
|
||||||
|
| `filledColor` | The fill color, used when `filled` is `true`. |
|
||||||
|
| `strokedColor` | The stroke color, used when `stroked` is `true`. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### SelectionAttributes
|
||||||
|
|
||||||
|
**Package:** `ovh.gasser.newshapes.attributes`
|
||||||
|
|
||||||
|
Tracks whether a shape is currently selected in the editor.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class SelectionAttributes implements Attributes {
|
||||||
|
|
||||||
|
public static final String ID = "SELECTION_ATTRS";
|
||||||
|
|
||||||
|
public boolean selected;
|
||||||
|
|
||||||
|
public SelectionAttributes();
|
||||||
|
|
||||||
|
public SelectionAttributes(boolean selected);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getID(); // returns "SELECTION_ATTRS"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Constructor | Description |
|
||||||
|
|-------------|-------------|
|
||||||
|
| `SelectionAttributes()` | Creates an instance with `selected` defaulting to `false`. |
|
||||||
|
| `SelectionAttributes(boolean selected)` | Creates an instance with an explicit initial selection state. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Selection (class)
|
||||||
|
|
||||||
|
**Package:** `ovh.gasser.newshapes`
|
||||||
|
|
||||||
|
Maintains the current set of selected shapes. Notifies registered `SelectionListener` instances when the selection changes. Implements `Streamable<Shape>`.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public class Selection implements Streamable<Shape> {
|
||||||
|
|
||||||
|
public void add(Shape s);
|
||||||
|
|
||||||
|
public void addAll(Collection<Shape> shapes);
|
||||||
|
|
||||||
|
public void clear();
|
||||||
|
|
||||||
|
public boolean isEmpty();
|
||||||
|
|
||||||
|
public List<Shape> getSelectedShapes();
|
||||||
|
|
||||||
|
public void addListener(SelectionListener listener);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Method | Description |
|
||||||
|
|--------|-------------|
|
||||||
|
| `add(Shape s)` | Adds a shape to the selection, marks it as selected via its `SelectionAttributes`, and notifies all listeners. |
|
||||||
|
| `addAll(Collection<Shape> shapes)` | Adds multiple shapes to the selection. |
|
||||||
|
| `clear()` | Deselects all shapes (updates their `SelectionAttributes`) and notifies all listeners. |
|
||||||
|
| `isEmpty()` | Returns `true` if no shapes are currently selected. |
|
||||||
|
| `getSelectedShapes()` | Returns an unmodifiable copy of the current selection list. |
|
||||||
|
| `addListener(SelectionListener listener)` | Registers a listener to be notified on selection changes. |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ResizeHandle (enum)
|
||||||
|
|
||||||
|
**Package:** `ovh.gasser.newshapes.shapes`
|
||||||
|
|
||||||
|
Identifies the eight cardinal and diagonal handles used when resizing a shape. Each constant maps to a standard AWT cursor type.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public enum ResizeHandle {
|
||||||
|
NW, N, NE,
|
||||||
|
W, E,
|
||||||
|
SW, S, SE;
|
||||||
|
|
||||||
|
public int getCursorType();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Value | Position | AWT Cursor |
|
||||||
|
|-------|----------|------------|
|
||||||
|
| `NW` | Top-left corner | `Cursor.NW_RESIZE_CURSOR` |
|
||||||
|
| `N` | Top edge center | `Cursor.N_RESIZE_CURSOR` |
|
||||||
|
| `NE` | Top-right corner | `Cursor.NE_RESIZE_CURSOR` |
|
||||||
|
| `E` | Right edge center | `Cursor.E_RESIZE_CURSOR` |
|
||||||
|
| `SE` | Bottom-right corner | `Cursor.SE_RESIZE_CURSOR` |
|
||||||
|
| `S` | Bottom edge center | `Cursor.S_RESIZE_CURSOR` |
|
||||||
|
| `SW` | Bottom-left corner | `Cursor.SW_RESIZE_CURSOR` |
|
||||||
|
| `W` | Left edge center | `Cursor.W_RESIZE_CURSOR` |
|
||||||
|
|
||||||
|
**`getCursorType()`** returns the `java.awt.Cursor` constant integer appropriate for the handle's position, suitable for use with `Component.setCursor(Cursor.getPredefinedCursor(...))`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Streamable (interface)
|
||||||
|
|
||||||
|
**Package:** `ovh.gasser.newshapes.util`
|
||||||
|
|
||||||
|
A convenience interface that extends `Iterable<T>` with a default `stream()` method, allowing any collection-like type in the model to be used directly in Java stream pipelines.
|
||||||
|
|
||||||
|
```java
|
||||||
|
public interface Streamable<T> extends Iterable<T> {
|
||||||
|
|
||||||
|
default Stream<T> stream() {
|
||||||
|
return StreamSupport.stream(spliterator(), false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Implemented by `SCollection` and `Selection`.
|
||||||
287
ARCHITECTURE.md
Normal file
287
ARCHITECTURE.md
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
```java
|
||||||
|
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:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 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 child
|
||||||
|
- `getBounds()` — returns the union of all children's bounding rectangles
|
||||||
|
- `clone()` — performs a deep copy by cloning each child recursively
|
||||||
|
- `accept(ShapeVisitor)` — calls `visitCollection`, 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:
|
||||||
|
|
||||||
|
```java
|
||||||
|
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.
|
||||||
|
|
||||||
|
```java
|
||||||
|
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.
|
||||||
84
CONTRIBUTING.md
Normal file
84
CONTRIBUTING.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Contributing Guide
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
|
||||||
|
- Java 16 or later (uses switch expressions, records, etc.)
|
||||||
|
- Maven 3.6+
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn compile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn exec:java -Dexec.mainClass=ovh.gasser.newshapes.App
|
||||||
|
```
|
||||||
|
|
||||||
|
Or run `ovh.gasser.newshapes.App.main()` from your IDE.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn test
|
||||||
|
```
|
||||||
|
|
||||||
|
Uses JUnit 5 (Jupiter). Tests are in `src/test/java/ovh/gasser/newshapes/`.
|
||||||
|
|
||||||
|
Current test coverage:
|
||||||
|
|
||||||
|
- Shape classes: `AbstractShapeTest`, `SRectangleTest`, `SCircleTest`, `STriangleTest`, `STextTest`
|
||||||
|
- Exporters: `SVGExporterTest`, `HTMLExporterTest`
|
||||||
|
- See `TESTING_HANDOFF.md` at the project root for detailed coverage analysis and recommendations.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
src/
|
||||||
|
├── main/java/ovh/gasser/newshapes/ — Application source
|
||||||
|
│ ├── shapes/ — Shape hierarchy
|
||||||
|
│ ├── attributes/ — Attribute types
|
||||||
|
│ ├── ui/ — Swing UI (view, controller, listeners)
|
||||||
|
│ │ ├── listeners/ — Menu and selection listeners
|
||||||
|
│ │ └── visitors/ — HTML/SVG visitor implementations
|
||||||
|
│ └── util/ — Utility interfaces
|
||||||
|
└── test/java/ovh/gasser/newshapes/ — JUnit 5 tests
|
||||||
|
├── shapes/ — Shape unit tests
|
||||||
|
└── exporters/ — Exporter integration tests
|
||||||
|
```
|
||||||
|
|
||||||
|
## Adding a New Shape
|
||||||
|
|
||||||
|
1. Create a class extending `AbstractShape` in `ovh.gasser.newshapes.shapes`.
|
||||||
|
2. Implement `accept(ShapeVisitor visitor)` — call `visitor.visitYourShape(this)`.
|
||||||
|
3. Implement `clone()` — return a deep copy.
|
||||||
|
4. Provide a static `create()` factory method.
|
||||||
|
5. Optionally override `resize()` if the shape needs custom resize behavior (e.g., `SCircle` maintains radius).
|
||||||
|
6. Add a `visitYourShape()` method to the `ShapeVisitor` interface.
|
||||||
|
7. Implement the new visit method in all three visitors: `ShapeDraftman`, `HTMLDraftman`, `SVGDraftman`.
|
||||||
|
8. Add a menu item in `App.buildFileMenu()` with a `MenuAddListener`.
|
||||||
|
9. Write tests.
|
||||||
|
|
||||||
|
## Adding a New Attribute
|
||||||
|
|
||||||
|
1. Create a class implementing `Attributes` in `ovh.gasser.newshapes.attributes`.
|
||||||
|
2. Define a `public static final String ID` constant.
|
||||||
|
3. Implement `getID()` returning the ID.
|
||||||
|
4. Use `shape.addAttributes(new YourAttribute(...))` to attach, and `shape.getAttributes(YourAttribute.ID)` to retrieve.
|
||||||
|
|
||||||
|
## Adding a New Exporter
|
||||||
|
|
||||||
|
1. Create a visitor class implementing `ShapeVisitor` in `ovh.gasser.newshapes.ui.visitors`.
|
||||||
|
2. Implement all visit methods to generate the target format.
|
||||||
|
3. Create an exporter class (like `HTMLExporter` / `SVGExporter`) that uses the visitor and writes to a file.
|
||||||
|
4. Wire it up in `App.buildFileMenu()`.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Static factory methods (`create()`) over public constructors.
|
||||||
|
- `getBounds()` always returns a defensive copy.
|
||||||
|
- Shapes are cloneable via the `clone()` method.
|
||||||
|
- Attributes are keyed by string IDs and stored in a `TreeMap`.
|
||||||
|
- Logging via SLF4J (`LoggerFactory.getLogger(YourClass.class)`).
|
||||||
100
README.md
Normal file
100
README.md
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
# Reactive Shapes
|
||||||
|
|
||||||
|
An interactive Java Swing application for creating, editing, and exporting 2D shapes.
|
||||||
|
|
||||||
|
**Authors**: Alexandre Colicchio and Thibaud Gasser
|
||||||
|
**Artifact**: `ovh.gasser:shapes:1.0-SNAPSHOT`
|
||||||
|
**Package**: `ovh.gasser.newshapes`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- **Shape creation** — rectangles, circles, triangles, and text labels
|
||||||
|
- **Selection** — click to select, shift-click for multi-select, drag to draw a selection box
|
||||||
|
- **Move** — drag selected shapes to reposition them on the canvas
|
||||||
|
- **Resize** — toggle resize mode with `R` to reveal 8 directional handles per shape
|
||||||
|
- **Color editing** — independently set fill and stroke colors on any selection
|
||||||
|
- **Export** — render the canvas to HTML/CSS or SVG
|
||||||
|
- **Composite shapes** — group shapes into named collections and manipulate them as a unit
|
||||||
|
- **Clone** — duplicate the current selection with `C`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Start
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Java 16 or later
|
||||||
|
- Maven 3.6 or later
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn compile
|
||||||
|
```
|
||||||
|
|
||||||
|
### Run
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn exec:java -Dexec.mainClass=ovh.gasser.newshapes.App
|
||||||
|
```
|
||||||
|
|
||||||
|
### Test
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mvn test
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Keyboard Shortcuts
|
||||||
|
|
||||||
|
| Key | Action |
|
||||||
|
|----------|---------------------------------|
|
||||||
|
| `R` | Toggle resize mode |
|
||||||
|
| `Delete` | Delete selected shapes |
|
||||||
|
| `C` | Clone the current selection |
|
||||||
|
| `A` | Randomize colors of selection |
|
||||||
|
| `H` | Export canvas to HTML |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Documentation Index
|
||||||
|
|
||||||
|
| Document | Description |
|
||||||
|
|---|---|
|
||||||
|
| [Architecture](ARCHITECTURE.md) | Design patterns, MVC structure, class relationships |
|
||||||
|
| [Shape System](SHAPES.md) | Shape hierarchy, attributes, and composite pattern |
|
||||||
|
| [Visitor & Exporters](VISITORS.md) | Visitor pattern, rendering, HTML/SVG export |
|
||||||
|
| [API Reference](API.md) | Public interfaces and key class reference |
|
||||||
|
| [Contributing](CONTRIBUTING.md) | Development setup, build, testing, conventions |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Project Status
|
||||||
|
|
||||||
|
See the [feature roadmap](../TODO.md) at the repository root for a full list of open and planned work.
|
||||||
|
|
||||||
|
**Completed**
|
||||||
|
|
||||||
|
- Text shapes
|
||||||
|
- Resize mode with 8-handle control points
|
||||||
|
|
||||||
|
**Planned**
|
||||||
|
|
||||||
|
- Undo/redo support
|
||||||
|
- Save and load canvas state
|
||||||
|
- Freeform polygon shapes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Tech Stack
|
||||||
|
|
||||||
|
| Component | Technology |
|
||||||
|
|-----------|------------|
|
||||||
|
| Language | Java 16 |
|
||||||
|
| Build | Maven |
|
||||||
|
| UI | Swing |
|
||||||
|
| Logging | SLF4J + Logback |
|
||||||
|
| Testing | JUnit 5 |
|
||||||
219
SHAPES.md
Normal file
219
SHAPES.md
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
# Shape System
|
||||||
|
|
||||||
|
## Shape Hierarchy
|
||||||
|
|
||||||
|
```
|
||||||
|
Shape (interface)
|
||||||
|
└── AbstractShape (abstract class)
|
||||||
|
├── SRectangle
|
||||||
|
├── SCircle
|
||||||
|
├── STriangle
|
||||||
|
├── SText
|
||||||
|
└── SCollection (Composite)
|
||||||
|
```
|
||||||
|
|
||||||
|
## The Shape Interface
|
||||||
|
|
||||||
|
Core contract defined in `ovh.gasser.newshapes.shapes.Shape`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
public interface Shape {
|
||||||
|
void accept(ShapeVisitor visitor);
|
||||||
|
void translate(int dx, int dy);
|
||||||
|
void resize(ResizeHandle handle, int dx, int dy);
|
||||||
|
Attributes getAttributes(String key);
|
||||||
|
void addAttributes(Attributes attr);
|
||||||
|
Rectangle getBounds();
|
||||||
|
Shape clone();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Every shape must support visitor dispatch, spatial transformation, attribute storage, bounds computation, and deep copying.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## AbstractShape -- Base Implementation
|
||||||
|
|
||||||
|
`ovh.gasser.newshapes.shapes.AbstractShape` provides common behavior for all concrete shapes.
|
||||||
|
|
||||||
|
### Bounds
|
||||||
|
|
||||||
|
A `protected final Rectangle bounds` field stores the shape's bounding rectangle. `getBounds()` always returns a **defensive copy** (`new Rectangle(this.bounds)`) to prevent external mutation.
|
||||||
|
|
||||||
|
### Attributes
|
||||||
|
|
||||||
|
Stored in a `TreeMap<String, Attributes>` keyed by `Attributes.getID()`. Lookup is O(log n). Attributes are added or replaced via `addAttributes()` and retrieved via `getAttributes(key)`.
|
||||||
|
|
||||||
|
### Translate
|
||||||
|
|
||||||
|
Delegates directly to `bounds.translate(dx, dy)`.
|
||||||
|
|
||||||
|
### Resize
|
||||||
|
|
||||||
|
Uses a switch expression over the `ResizeHandle` enum to determine which edges to adjust:
|
||||||
|
|
||||||
|
| Handle | X Adjustment | Y Adjustment | Width Adjustment | Height Adjustment |
|
||||||
|
|--------|-------------|-------------|-----------------|-------------------|
|
||||||
|
| N | -- | `+dy` | -- | `-dy` |
|
||||||
|
| S | -- | -- | -- | `+dy` |
|
||||||
|
| E | -- | -- | `+dx` | -- |
|
||||||
|
| W | `+dx` | -- | `-dx` | -- |
|
||||||
|
| NE | -- | `+dy` | `+dx` | `-dy` |
|
||||||
|
| NW | `+dx` | `+dy` | `-dx` | `-dy` |
|
||||||
|
| SE | -- | -- | `+dx` | `+dy` |
|
||||||
|
| SW | `+dx` | -- | `-dx` | `+dy` |
|
||||||
|
|
||||||
|
After resizing, width and height are clamped to a minimum of 1 pixel.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Concrete Shapes
|
||||||
|
|
||||||
|
### SRectangle
|
||||||
|
|
||||||
|
The simplest shape. Uses `AbstractShape`'s bounds directly with no overrides for resize or bounds computation.
|
||||||
|
|
||||||
|
**Factory methods:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
SRectangle.create(int x, int y, int width, int height)
|
||||||
|
SRectangle.create(int x, int y, int width, int height, Color color)
|
||||||
|
```
|
||||||
|
|
||||||
|
### SCircle
|
||||||
|
|
||||||
|
Maintains a `radius` field alongside the inherited bounds.
|
||||||
|
|
||||||
|
- **`resize()`**: Overridden to adjust radius while keeping circular proportions.
|
||||||
|
- **`getBounds()`**: Computed from center and radius: `new Rectangle(x - radius, y - radius, 2 * radius, 2 * radius)`.
|
||||||
|
|
||||||
|
**Factory methods:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
SCircle.create(int x, int y, int radius)
|
||||||
|
SCircle.create(int x, int y, int radius, Color fillColor)
|
||||||
|
```
|
||||||
|
|
||||||
|
### STriangle
|
||||||
|
|
||||||
|
Equilateral triangle defined by position and size.
|
||||||
|
|
||||||
|
- **`resize()`**: Overridden to maintain equilateral proportions based on a single size parameter.
|
||||||
|
|
||||||
|
**Factory method:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
STriangle.create(int x, int y, int size, Color fillColor, Color strokeColor)
|
||||||
|
```
|
||||||
|
|
||||||
|
### SText
|
||||||
|
|
||||||
|
Text shape with font properties (`fontName`, `fontStyle`, `fontSize`). Displays a `"Click to edit"` placeholder when text is null or empty.
|
||||||
|
|
||||||
|
**Factory methods:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
SText.create(int x, int y, String text) // Default: SansSerif, PLAIN, 16
|
||||||
|
SText.create(int x, int y, String text, String fontName, int fontStyle, int fontSize) // Custom font
|
||||||
|
```
|
||||||
|
|
||||||
|
### SCollection -- Composite Pattern
|
||||||
|
|
||||||
|
Groups shapes into a tree structure. Extends `AbstractShape` and implements `Streamable<Shape>` (which extends `Iterable<Shape>`).
|
||||||
|
|
||||||
|
**Key behaviors:**
|
||||||
|
|
||||||
|
| Method | Behavior |
|
||||||
|
|-------------------|-----------------------------------------------------------------------------------------------|
|
||||||
|
| `getBounds()` | Union of all children's bounds. Returns `App.WIN_SIZE` if the collection is empty. |
|
||||||
|
| `translate(dx,dy)`| Propagates to all children (does **not** use `AbstractShape`'s bounds). |
|
||||||
|
| `clone()` | Deep copies all children into a new collection. |
|
||||||
|
| `addAttributes()` | Propagates `ColorAttributes` to all children. Stores other attribute types locally via super. |
|
||||||
|
| `getAttributes()` | For `ColorAttributes`, returns the first child's attributes. Other types use super. |
|
||||||
|
| `add(Shape)` | Appends a child shape. |
|
||||||
|
| `remove(Shape)` | Removes a child shape; logs an error if the shape is not found. |
|
||||||
|
|
||||||
|
**Factory method:**
|
||||||
|
|
||||||
|
```java
|
||||||
|
SCollection.of(Shape... shapes) // Auto-adds SelectionAttributes
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Attributes System
|
||||||
|
|
||||||
|
### Design
|
||||||
|
|
||||||
|
Attributes are stored as a map in `AbstractShape`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
private final Map<String, Attributes> attributes = new TreeMap<>();
|
||||||
|
```
|
||||||
|
|
||||||
|
Each attribute type defines a unique string ID. This provides an extensible, type-safe way to attach arbitrary metadata to shapes without modifying the shape classes.
|
||||||
|
|
||||||
|
### Built-in Attributes
|
||||||
|
|
||||||
|
#### ColorAttributes
|
||||||
|
|
||||||
|
ID: `"COLOR_ATTRS"`
|
||||||
|
|
||||||
|
Controls shape rendering appearance:
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|----------------|-----------|--------------------------------------|
|
||||||
|
| `filled` | `boolean` | Whether the shape interior is filled |
|
||||||
|
| `stroked` | `boolean` | Whether the shape outline is drawn |
|
||||||
|
| `filledColor` | `Color` | Interior fill color |
|
||||||
|
| `strokedColor` | `Color` | Stroke/border color |
|
||||||
|
|
||||||
|
```java
|
||||||
|
new ColorAttributes(boolean filled, boolean stroked, Color filledColor, Color strokedColor)
|
||||||
|
```
|
||||||
|
|
||||||
|
#### SelectionAttributes
|
||||||
|
|
||||||
|
ID: `"SELECTION_ATTRS"`
|
||||||
|
|
||||||
|
Tracks whether a shape is currently selected:
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
|------------|-----------|---------------------------|
|
||||||
|
| `selected` | `boolean` | Current selection state |
|
||||||
|
|
||||||
|
```java
|
||||||
|
new SelectionAttributes() // Default: false
|
||||||
|
new SelectionAttributes(true) // Explicitly selected
|
||||||
|
```
|
||||||
|
|
||||||
|
Added automatically by shape factory methods and by `Selection.add()`.
|
||||||
|
|
||||||
|
### Extending the System
|
||||||
|
|
||||||
|
To add a new attribute type:
|
||||||
|
|
||||||
|
1. Implement the `Attributes` interface.
|
||||||
|
2. Define a `public static final String ID` constant.
|
||||||
|
3. Implement `getID()` returning the ID.
|
||||||
|
4. Attach with `shape.addAttributes(new MyAttribute(...))`.
|
||||||
|
5. Retrieve with `(MyAttribute) shape.getAttributes(MyAttribute.ID)`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ResizeHandle Enum
|
||||||
|
|
||||||
|
Eight directional handles for interactive resizing:
|
||||||
|
|
||||||
|
| Value | Cursor Type |
|
||||||
|
|-------|-------------------------|
|
||||||
|
| `NW` | `Cursor.NW_RESIZE_CURSOR` |
|
||||||
|
| `N` | `Cursor.N_RESIZE_CURSOR` |
|
||||||
|
| `NE` | `Cursor.NE_RESIZE_CURSOR` |
|
||||||
|
| `E` | `Cursor.E_RESIZE_CURSOR` |
|
||||||
|
| `SE` | `Cursor.SE_RESIZE_CURSOR` |
|
||||||
|
| `S` | `Cursor.S_RESIZE_CURSOR` |
|
||||||
|
| `SW` | `Cursor.SW_RESIZE_CURSOR` |
|
||||||
|
| `W` | `Cursor.W_RESIZE_CURSOR` |
|
||||||
|
|
||||||
|
Used by `AbstractShape.resize()` to determine which bounds edges to adjust, and by `Controller` to set the mouse cursor during resize interactions.
|
||||||
188
VISITORS.md
Normal file
188
VISITORS.md
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
# 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
|
||||||
|
|
||||||
|
```java
|
||||||
|
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:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// In SRectangle:
|
||||||
|
@Override
|
||||||
|
public void accept(ShapeVisitor visitor) {
|
||||||
|
visitor.visitRectangle(this);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
For `SCollection`, the visitor iterates over children to traverse the tree:
|
||||||
|
|
||||||
|
```java
|
||||||
|
// 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`) using `Graphics2D`.
|
||||||
|
- Reads `ColorAttributes` to 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
|
||||||
|
|
||||||
|
```java
|
||||||
|
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
|
||||||
|
|
||||||
|
```java
|
||||||
|
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`):
|
||||||
|
|
||||||
|
```java
|
||||||
|
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
|
||||||
|
|
||||||
|
```java
|
||||||
|
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()`:
|
||||||
|
|
||||||
|
```java
|
||||||
|
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 of `sTriangle.hashCode()` for CSS class naming. This is a bug that could cause CSS class collisions when multiple triangles are exported.
|
||||||
Reference in New Issue
Block a user