From 731a57b080f9baa4d03ba3513d642345b410beaa Mon Sep 17 00:00:00 2001 From: Thibaud Date: Fri, 27 Mar 2026 15:43:53 +0100 Subject: [PATCH] add generated docs --- API.md | 414 ++++++++++++++++++++++++++++++++++++++++++++++++ ARCHITECTURE.md | 287 +++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 84 ++++++++++ README.md | 100 ++++++++++++ SHAPES.md | 219 +++++++++++++++++++++++++ VISITORS.md | 188 ++++++++++++++++++++++ 6 files changed, 1292 insertions(+) create mode 100644 API.md create mode 100644 ARCHITECTURE.md create mode 100644 CONTRIBUTING.md create mode 100644 README.md create mode 100644 SHAPES.md create mode 100644 VISITORS.md diff --git a/API.md b/API.md new file mode 100644 index 0000000..1815a9a --- /dev/null +++ b/API.md @@ -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 + + @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` and `Iterable`. Overrides core `Shape` methods to propagate operations to all children. + +```java +public class SCollection extends AbstractShape + implements Streamable, Iterable { + + 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`. + +```java +public class Selection implements Streamable { + + public void add(Shape s); + + public void addAll(Collection shapes); + + public void clear(); + + public boolean isEmpty(); + + public List 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 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` 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 extends Iterable { + + default Stream stream() { + return StreamSupport.stream(spliterator(), false); + } +} +``` + +Implemented by `SCollection` and `Selection`. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md new file mode 100644 index 0000000..68ea471 --- /dev/null +++ b/ARCHITECTURE.md @@ -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 `
` elements with inline CSS for each shape | +| `SVGDraftman` | Emits SVG primitives (``, ``, ``, ``) | + +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 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`, providing a `stream()` method that exposes the +child list as a `Stream` for functional-style iteration. + +### 4.3 Observer Pattern + +`Selection` maintains the set of currently selected shapes and a list of `SelectionListener` +callbacks. + +``` +Selection + ├── List selectedShapes + ├── List 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` 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 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. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..a94719b --- /dev/null +++ b/CONTRIBUTING.md @@ -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)`). diff --git a/README.md b/README.md new file mode 100644 index 0000000..b7c1615 --- /dev/null +++ b/README.md @@ -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 | diff --git a/SHAPES.md b/SHAPES.md new file mode 100644 index 0000000..7444553 --- /dev/null +++ b/SHAPES.md @@ -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` 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` (which extends `Iterable`). + +**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 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. diff --git a/VISITORS.md b/VISITORS.md new file mode 100644 index 0000000..28cea3b --- /dev/null +++ b/VISITORS.md @@ -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 `
` elements with CSS for absolute positioning. + +**Output format:** + +| Shape | HTML Representation | +|-------------|--------------------------------------------------------------------------| +| Rectangle | `
` with `position: absolute`, `left`, `top`, `width`, `height` | +| Circle | `
` with `border-radius: 50%` | +| Triangle | `
` using CSS border tricks | +| Text | Styled `
` 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 | `` | +| Circle | `` | +| Triangle | `` with computed vertex coordinates | +| Text | `` with font attributes | +| Collection | `` 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 (`` 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.