From b34ad6a2e4de084456f4c25884762a56d1f0cf04 Mon Sep 17 00:00:00 2001 From: Thibaud Date: Thu, 19 Mar 2026 22:20:23 +0100 Subject: [PATCH] implement resize feature --- .../java/ovh/gasser/newshapes/Selection.java | 4 + .../newshapes/shapes/AbstractShape.java | 39 ++++++++ .../gasser/newshapes/shapes/ResizeHandle.java | 24 +++++ .../ovh/gasser/newshapes/shapes/SCircle.java | 28 +++++- .../gasser/newshapes/shapes/STriangle.java | 48 ++++++++++ .../ovh/gasser/newshapes/shapes/Shape.java | 1 + .../ovh/gasser/newshapes/ui/Controller.java | 89 +++++++++++++++++-- .../gasser/newshapes/ui/ShapeDraftman.java | 19 +++- .../ovh/gasser/newshapes/ui/ShapesView.java | 6 ++ 9 files changed, 249 insertions(+), 9 deletions(-) create mode 100644 src/main/java/ovh/gasser/newshapes/shapes/ResizeHandle.java diff --git a/src/main/java/ovh/gasser/newshapes/Selection.java b/src/main/java/ovh/gasser/newshapes/Selection.java index c6b8896..6029c35 100644 --- a/src/main/java/ovh/gasser/newshapes/Selection.java +++ b/src/main/java/ovh/gasser/newshapes/Selection.java @@ -38,6 +38,10 @@ public class Selection implements Streamable { notifyListeners(); } + public boolean isEmpty() { + return selectedShapes.isEmpty(); + } + public List getSelectedShapes() { return List.copyOf(selectedShapes); } diff --git a/src/main/java/ovh/gasser/newshapes/shapes/AbstractShape.java b/src/main/java/ovh/gasser/newshapes/shapes/AbstractShape.java index 7affa99..72eecff 100644 --- a/src/main/java/ovh/gasser/newshapes/shapes/AbstractShape.java +++ b/src/main/java/ovh/gasser/newshapes/shapes/AbstractShape.java @@ -34,6 +34,45 @@ public abstract class AbstractShape implements Shape { getBounds().translate(dx, dy); } + @Override + public void resize(ResizeHandle handle, int dx, int dy) { + Rectangle bounds = getBounds(); + switch (handle) { + case E -> bounds.width += dx; + case W -> { + bounds.x += dx; + bounds.width -= dx; + } + case S -> bounds.height += dy; + case N -> { + bounds.y += dy; + bounds.height -= dy; + } + case SE -> { + bounds.width += dx; + bounds.height += dy; + } + case SW -> { + bounds.x += dx; + bounds.width -= dx; + bounds.height += dy; + } + case NE -> { + bounds.width += dx; + bounds.y += dy; + bounds.height -= dy; + } + case NW -> { + bounds.x += dx; + bounds.width -= dx; + bounds.y += dy; + bounds.height -= dy; + } + } + if (bounds.width < 1) bounds.width = 1; + if (bounds.height < 1) bounds.height = 1; + } + @Override public Rectangle getBounds() { return this.bounds; diff --git a/src/main/java/ovh/gasser/newshapes/shapes/ResizeHandle.java b/src/main/java/ovh/gasser/newshapes/shapes/ResizeHandle.java new file mode 100644 index 0000000..e826e69 --- /dev/null +++ b/src/main/java/ovh/gasser/newshapes/shapes/ResizeHandle.java @@ -0,0 +1,24 @@ +package ovh.gasser.newshapes.shapes; + +import java.awt.Cursor; + +public enum ResizeHandle { + 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); + + private final int cursorType; + + ResizeHandle(int cursorType) { + this.cursorType = cursorType; + } + + public int getCursorType() { + return cursorType; + } +} \ No newline at end of file diff --git a/src/main/java/ovh/gasser/newshapes/shapes/SCircle.java b/src/main/java/ovh/gasser/newshapes/shapes/SCircle.java index 568a4ca..d1e2f84 100644 --- a/src/main/java/ovh/gasser/newshapes/shapes/SCircle.java +++ b/src/main/java/ovh/gasser/newshapes/shapes/SCircle.java @@ -8,7 +8,7 @@ import java.awt.*; public class SCircle extends AbstractShape { - private final int radius; + private int radius; private SCircle(int x, int y, int radius) { super(new Rectangle(x, y, radius * 2, radius * 2)); @@ -20,6 +20,32 @@ public class SCircle extends AbstractShape { visitor.visitCircle(this); } + @Override + public void resize(ResizeHandle handle, int dx, int dy) { + Rectangle bounds = getBounds(); + int newWidth = bounds.width; + int newHeight = bounds.height; + + switch (handle) { + case E, W -> newWidth += dx; + case N, S -> newHeight += dy; + case SE, NW -> { + newWidth += dx; + newHeight += dy; + } + case NE, SW -> { + newWidth += dx; + newHeight += dy; + } + } + + if (newWidth < 2) newWidth = 2; + if (newHeight < 2) newHeight = 2; + + this.radius = Math.max(newWidth, newHeight) / 2; + bounds.setSize(this.radius * 2, this.radius * 2); + } + @Override public Shape clone() { var color = (ColorAttributes) getAttributes(ColorAttributes.ID); diff --git a/src/main/java/ovh/gasser/newshapes/shapes/STriangle.java b/src/main/java/ovh/gasser/newshapes/shapes/STriangle.java index fc2afd6..af19f28 100644 --- a/src/main/java/ovh/gasser/newshapes/shapes/STriangle.java +++ b/src/main/java/ovh/gasser/newshapes/shapes/STriangle.java @@ -16,6 +16,54 @@ public class STriangle extends AbstractShape { visitor.visitTriangle(this); } + @Override + public void resize(ResizeHandle handle, int dx, int dy) { + Rectangle bounds = getBounds(); + int delta = Math.max(Math.abs(dx), Math.abs(dy)); + + boolean shrink = switch (handle) { + case SE -> (dx < 0 || dy < 0); + case NW -> (dx > 0 || dy > 0); + case NE -> (dx < 0); + case SW -> (dx > 0); + case E, W -> (dx < 0); + case N, S -> (dy < 0); + default -> false; + }; + + int sizeChange = shrink ? -delta : delta; + + switch (handle) { + case SE, E, W -> { + bounds.width += sizeChange; + bounds.height += sizeChange; + } + case NW -> { + bounds.x -= sizeChange; + bounds.width += sizeChange; + bounds.y -= sizeChange; + bounds.height += sizeChange; + } + case NE -> { + bounds.y -= sizeChange; + bounds.width += sizeChange; + bounds.height += sizeChange; + } + case SW -> { + bounds.x -= sizeChange; + bounds.width += sizeChange; + bounds.height += sizeChange; + } + case N, S -> { + bounds.width += sizeChange; + bounds.height += sizeChange; + } + } + + if (bounds.width < 1) bounds.width = 1; + if (bounds.height < 1) bounds.height = 1; + } + @Override public Shape clone() { var color = (ColorAttributes) getAttributes(ColorAttributes.ID); diff --git a/src/main/java/ovh/gasser/newshapes/shapes/Shape.java b/src/main/java/ovh/gasser/newshapes/shapes/Shape.java index 59da208..9cc70e2 100644 --- a/src/main/java/ovh/gasser/newshapes/shapes/Shape.java +++ b/src/main/java/ovh/gasser/newshapes/shapes/Shape.java @@ -8,6 +8,7 @@ import java.awt.*; 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(); diff --git a/src/main/java/ovh/gasser/newshapes/ui/Controller.java b/src/main/java/ovh/gasser/newshapes/ui/Controller.java index 9dc2f24..2b8d22f 100644 --- a/src/main/java/ovh/gasser/newshapes/ui/Controller.java +++ b/src/main/java/ovh/gasser/newshapes/ui/Controller.java @@ -5,6 +5,7 @@ import org.slf4j.LoggerFactory; import ovh.gasser.newshapes.HTMLExporter; import ovh.gasser.newshapes.Selection; import ovh.gasser.newshapes.attributes.ColorAttributes; +import ovh.gasser.newshapes.shapes.ResizeHandle; import ovh.gasser.newshapes.shapes.SCollection; import ovh.gasser.newshapes.shapes.SText; import ovh.gasser.newshapes.shapes.Shape; @@ -27,6 +28,10 @@ public class Controller { private Point lastMousePos; private boolean addingText; + private boolean resizing; + private ResizeHandle activeHandle; + private Point resizeOrigin; + private boolean resizeMode; Controller(ShapesView view, SCollection model) { this.view = view; @@ -43,6 +48,13 @@ public class Controller { public void mouseDragged(MouseEvent evt) { handleMouseDragged(evt); } + + @Override + public void mouseReleased(MouseEvent evt) { + resizing = false; + activeHandle = null; + resizeOrigin = null; + } }; this.view.addMouseMotionListener(adapter); this.view.addMouseListener(adapter); @@ -55,12 +67,31 @@ public class Controller { } private void handleMouseDragged(MouseEvent evt) { - int dx = evt.getX() - lastMousePos.x; - int dy = evt.getY() - lastMousePos.y; - for (Shape shape : selection) { - shape.translate(dx, dy); + if (resizeMode && resizing && activeHandle != null) { + int dx = evt.getX() - resizeOrigin.x; + int dy = evt.getY() - resizeOrigin.y; + for (Shape shape : selection) { + shape.resize(activeHandle, dx, dy); + } + resizeOrigin = evt.getPoint(); + } else if (resizeMode && !selection.isEmpty()) { + lastMousePos = evt.getPoint(); + ResizeHandle handle = getHandleAt(evt.getPoint()); + if (handle != null) { + resizing = true; + activeHandle = handle; + resizeOrigin = evt.getPoint(); + } + } else { + resizing = false; + activeHandle = null; + int dx = evt.getX() - lastMousePos.x; + int dy = evt.getY() - lastMousePos.y; + for (Shape shape : selection) { + shape.translate(dx, dy); + } + lastMousePos = evt.getPoint(); } - lastMousePos = evt.getPoint(); view.repaint(); } @@ -70,6 +101,21 @@ public class Controller { return; } + resizing = false; + activeHandle = null; + resizeOrigin = null; + + if (resizeMode && !selection.isEmpty()) { + ResizeHandle handle = getHandleAt(evt.getPoint()); + if (handle != null) { + resizing = true; + activeHandle = handle; + resizeOrigin = evt.getPoint(); + view.repaint(); + return; + } + } + getTarget(evt, this.model) .ifPresentOrElse( s -> { @@ -85,6 +131,7 @@ public class Controller { view.repaint(); } +<<<<<<< HEAD public void enterTextMode() { addingText = true; } @@ -101,8 +148,32 @@ public class Controller { view.repaint(); } + public void enterTextMode() { + addingText = true; + } + + private ResizeHandle getHandleAt(Point point) { + final int handleSize = 5; + for (Shape shape : selection) { + Rectangle bounds = shape.getBounds(); + int cx = bounds.x + bounds.width / 2; + int cy = bounds.y + bounds.height / 2; + + if (point.x < cx && point.y < cy) return ResizeHandle.NW; + if (point.x > cx && point.y < cy) return ResizeHandle.NE; + if (point.x < cx && point.y > cy) return ResizeHandle.SW; + if (point.x > cx && point.y > cy) return ResizeHandle.SE; + if (point.y < cy) return ResizeHandle.N; + if (point.y > cy) return ResizeHandle.S; + if (point.x < cx) return ResizeHandle.W; + if (point.x > cx) return ResizeHandle.E; + } + return null; + } + private void handleKeyPressed(KeyEvent evt) { switch (evt.getKeyCode()) { + case KeyEvent.VK_R -> toggleResizeMode(); case KeyEvent.VK_DELETE -> deleteSelected(); case KeyEvent.VK_C -> copySelection(); case KeyEvent.VK_A -> changeSelectionColor(); @@ -111,6 +182,14 @@ public class Controller { } } + private void toggleResizeMode() { + if (!selection.isEmpty()) { + resizeMode = !resizeMode; + view.setResizeMode(resizeMode); + view.repaint(); + } + } + private void exportHtml() { logger.info("Exporting view to html"); try { diff --git a/src/main/java/ovh/gasser/newshapes/ui/ShapeDraftman.java b/src/main/java/ovh/gasser/newshapes/ui/ShapeDraftman.java index a98c657..cd1bc55 100644 --- a/src/main/java/ovh/gasser/newshapes/ui/ShapeDraftman.java +++ b/src/main/java/ovh/gasser/newshapes/ui/ShapeDraftman.java @@ -13,12 +13,16 @@ public class ShapeDraftman implements ShapeVisitor { private static final ColorAttributes DEFAULT_COLOR_ATTRIBUTES = new ColorAttributes(false, true, Color.BLACK, Color.BLACK); private final Graphics2D g2d; - + private boolean resizeMode; public ShapeDraftman(Graphics graph) { this.g2d = (Graphics2D) graph; } + public void setResizeMode(boolean resizeMode) { + this.resizeMode = resizeMode; + } + @Override public void visitRectangle(SRectangle rect) { Rectangle r = rect.getBounds(); @@ -129,8 +133,17 @@ public class ShapeDraftman implements ShapeVisitor { if ((selAttrs != null) && (selAttrs.selected)){ Rectangle bounds = s.getBounds(); this.g2d.setColor(Color.RED); - this.g2d.drawRect(bounds.x - 5, bounds.y - 5, 5, 5); - this.g2d.drawRect(bounds.x + bounds.width, bounds.y + bounds.height, 5, 5); + int handleSize = 5; + this.g2d.drawRect(bounds.x - handleSize, bounds.y - handleSize, handleSize, handleSize); + this.g2d.drawRect(bounds.x + bounds.width, bounds.y + bounds.height, handleSize, handleSize); + if (resizeMode) { + this.g2d.drawRect(bounds.x + bounds.width, bounds.y - handleSize, handleSize, handleSize); + this.g2d.drawRect(bounds.x - handleSize, bounds.y + bounds.height, handleSize, handleSize); + this.g2d.drawRect(bounds.x + bounds.width / 2 - handleSize / 2, bounds.y - handleSize, handleSize, handleSize); + this.g2d.drawRect(bounds.x + bounds.width / 2 - handleSize / 2, bounds.y + bounds.height, handleSize, handleSize); + this.g2d.drawRect(bounds.x - handleSize, bounds.y + bounds.height / 2 - handleSize / 2, handleSize, handleSize); + this.g2d.drawRect(bounds.x + bounds.width, bounds.y + bounds.height / 2 - handleSize / 2, handleSize, handleSize); + } } } diff --git a/src/main/java/ovh/gasser/newshapes/ui/ShapesView.java b/src/main/java/ovh/gasser/newshapes/ui/ShapesView.java index 7881816..0ef9b33 100644 --- a/src/main/java/ovh/gasser/newshapes/ui/ShapesView.java +++ b/src/main/java/ovh/gasser/newshapes/ui/ShapesView.java @@ -12,6 +12,7 @@ public class ShapesView extends JPanel { private final Shape model; private final Controller controller; private ShapeVisitor draftman; + private boolean resizeMode; public ShapesView(SCollection model) { this.model = model; @@ -22,6 +23,7 @@ public class ShapesView extends JPanel { protected void paintComponent(Graphics g) { super.paintComponent(g); this.draftman = new ShapeDraftman(g); + ((ShapeDraftman) this.draftman).setResizeMode(resizeMode); model.accept(draftman); } @@ -32,4 +34,8 @@ public class ShapesView extends JPanel { public void addSelectionChangeListener(SelectionListener listener) { controller.addSelectionChangeListener(listener); } + + public void setResizeMode(boolean resizeMode) { + this.resizeMode = resizeMode; + } }