1
SHAPES
Thibaud edited this page 2026-03-27 15:43:53 +01:00

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:

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:

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:

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:

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:

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:

SCollection.of(Shape... shapes)  // Auto-adds SelectionAttributes

Attributes System

Design

Attributes are stored as a map in AbstractShape:

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