add generated docs

2026-03-27 15:43:53 +01:00
parent 269a6f8e40
commit 731a57b080
6 changed files with 1292 additions and 0 deletions

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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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.